From 2caeabc3d81e4cf3bfeb0b8cf65f54e9dc443a16 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 10:34:35 +0100 Subject: [PATCH 01/28] add black formatter configuration --- pyproject.toml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aa35b6212..bdf6c25d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,9 +41,27 @@ rdd = [ dev = [ "pytest", "xgboost", - "lightgbm" + "lightgbm", + "black>=24.3.0", ] +[tool.black] +line-length = 127 +target-version = ['py39', 'py310', 'py311'] +preview = true +exclude = ''' +/( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.mypy_cache + | \.vscode + | build + | dist + | doc/_build + | doubleml/externals +)/ +''' + [project.urls] Documentation = "https://docs.doubleml.org" Source = "https://github.com/DoubleML/doubleml-for-py" From cebd465cdae5a2582b8e7812b374322b6d58087b Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 10:43:59 +0100 Subject: [PATCH 02/28] add ruff configuration --- pyproject.toml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bdf6c25d1..7388adf18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dev = [ "xgboost", "lightgbm", "black>=24.3.0", + "ruff>=0.5.1", ] [tool.black] @@ -62,6 +63,42 @@ exclude = ''' )/ ''' +[tool.ruff] +# max line length for black +line-length = 127 +target-version = "py39" +exclude = [ + ".git", + ".mypy_cache", + ".vscode", + "build", + "dist", + "doc/_build", + "doc/auto_examples", + "doubleml/externals", +] + +[tool.ruff.lint] +# This enables us to use CPY001: copyright header check +preview = true +# This enables us to use the explicit preview rules that we want only +explicit-preview-rules = true +# all rules can be found here: https://beta.ruff.rs/docs/rules/ +select = ["E", "F", "W", "I", "CPY001"] +ignore = [ + # space before : (needed for how black formats slicing) + "E203", + # do not assign a lambda expression, use a def + "E731", + # E721 is in preview (july 2024) and gives many false positives. + # Use `is` and `is not` for type comparisons, or `isinstance()` for + # isinstance checks + "E721", + # F841 is in preview (july 2024), and we don't care much about it. + # Local variable ... is assigned to but never used + "F841", +] + [project.urls] Documentation = "https://docs.doubleml.org" Source = "https://github.com/DoubleML/doubleml-for-py" From f369cb24989541d6e425a6a51e16dab927ab226d Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 10:50:48 +0100 Subject: [PATCH 03/28] update ruff configuration --- pyproject.toml | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7388adf18..a6853b0b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,25 +79,9 @@ exclude = [ ] [tool.ruff.lint] -# This enables us to use CPY001: copyright header check -preview = true -# This enables us to use the explicit preview rules that we want only -explicit-preview-rules = true # all rules can be found here: https://beta.ruff.rs/docs/rules/ -select = ["E", "F", "W", "I", "CPY001"] -ignore = [ - # space before : (needed for how black formats slicing) - "E203", - # do not assign a lambda expression, use a def - "E731", - # E721 is in preview (july 2024) and gives many false positives. - # Use `is` and `is not` for type comparisons, or `isinstance()` for - # isinstance checks - "E721", - # F841 is in preview (july 2024), and we don't care much about it. - # Local variable ... is assigned to but never used - "F841", -] +select = ["E", "F", "W", "I"] +ignore = [] [project.urls] Documentation = "https://docs.doubleml.org" From 5751524e7bd20d3b33f9338c010b89bf661616e9 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 10:51:32 +0100 Subject: [PATCH 04/28] run ruff for did --- doubleml/did/did.py | 8 ++++---- doubleml/did/did_cs.py | 8 ++++---- doubleml/did/tests/_utils_did_manual.py | 2 +- doubleml/did/tests/test_did.py | 9 ++++----- doubleml/did/tests/test_did_cs.py | 7 +++---- doubleml/did/tests/test_did_cs_external_predictions.py | 7 +++++-- doubleml/did/tests/test_did_cs_tune.py | 9 ++++----- doubleml/did/tests/test_did_external_predictions.py | 7 +++++-- doubleml/did/tests/test_did_tune.py | 9 ++++----- 9 files changed, 34 insertions(+), 32 deletions(-) diff --git a/doubleml/did/did.py b/doubleml/did/did.py index c62c8e329..ed61ace69 100644 --- a/doubleml/did/did.py +++ b/doubleml/did/did.py @@ -1,14 +1,14 @@ +import warnings + import numpy as np from sklearn.utils import check_X_y from sklearn.utils.multiclass import type_of_target -import warnings from ..double_ml import DoubleML from ..double_ml_data import DoubleMLData from ..double_ml_score_mixins import LinearScoreMixin - -from ..utils._estimation import _dml_cv_predict, _get_cond_smpls, _dml_tune, _trimm -from ..utils._checks import _check_score, _check_trimming, _check_finite_predictions, _check_is_propensity +from ..utils._checks import _check_finite_predictions, _check_is_propensity, _check_score, _check_trimming +from ..utils._estimation import _dml_cv_predict, _dml_tune, _get_cond_smpls, _trimm class DoubleMLDID(LinearScoreMixin, DoubleML): diff --git a/doubleml/did/did_cs.py b/doubleml/did/did_cs.py index b1cd3d4f5..a5c9780b3 100644 --- a/doubleml/did/did_cs.py +++ b/doubleml/did/did_cs.py @@ -1,14 +1,14 @@ +import warnings + import numpy as np from sklearn.utils import check_X_y from sklearn.utils.multiclass import type_of_target -import warnings from ..double_ml import DoubleML from ..double_ml_data import DoubleMLData from ..double_ml_score_mixins import LinearScoreMixin - -from ..utils._estimation import _dml_cv_predict, _trimm, _get_cond_smpls_2d, _dml_tune -from ..utils._checks import _check_score, _check_trimming, _check_finite_predictions, _check_is_propensity +from ..utils._checks import _check_finite_predictions, _check_is_propensity, _check_score, _check_trimming +from ..utils._estimation import _dml_cv_predict, _dml_tune, _get_cond_smpls_2d, _trimm class DoubleMLDIDCS(LinearScoreMixin, DoubleML): diff --git a/doubleml/did/tests/_utils_did_manual.py b/doubleml/did/tests/_utils_did_manual.py index 0fc98a6a2..cb833b25b 100644 --- a/doubleml/did/tests/_utils_did_manual.py +++ b/doubleml/did/tests/_utils_did_manual.py @@ -1,8 +1,8 @@ import numpy as np from sklearn.base import clone -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, fit_predict_proba, tune_grid_search +from ...tests._utils_boot import boot_manual, draw_weights def fit_did(y, x, d, diff --git a/doubleml/did/tests/test_did.py b/doubleml/did/tests/test_did.py index 88e8539ea..872ac9acf 100644 --- a/doubleml/did/tests/test_did.py +++ b/doubleml/did/tests/test_did.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_did_manual import fit_did, boot_did, fit_sensitivity_elements_did +from ._utils_did_manual import boot_did, fit_did, fit_sensitivity_elements_did @pytest.fixture(scope='module', diff --git a/doubleml/did/tests/test_did_cs.py b/doubleml/did/tests/test_did_cs.py index df3460b48..d9f63e9ca 100644 --- a/doubleml/did/tests/test_did_cs.py +++ b/doubleml/did/tests/test_did_cs.py @@ -1,11 +1,10 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml diff --git a/doubleml/did/tests/test_did_cs_external_predictions.py b/doubleml/did/tests/test_did_cs_external_predictions.py index 732aaa54d..20060c4ce 100644 --- a/doubleml/did/tests/test_did_cs_external_predictions.py +++ b/doubleml/did/tests/test_did_cs_external_predictions.py @@ -1,10 +1,13 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LinearRegression, LogisticRegression + from doubleml import DoubleMLDIDCS from doubleml.datasets import make_did_SZ2020 -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor + from ...tests._utils import draw_smpls diff --git a/doubleml/did/tests/test_did_cs_tune.py b/doubleml/did/tests/test_did_cs_tune.py index bc8abec84..da5edafe6 100644 --- a/doubleml/did/tests/test_did_cs_tune.py +++ b/doubleml/did/tests/test_did_cs_tune.py @@ -1,17 +1,16 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_did_manual import boot_did from ._utils_did_cs_manual import fit_did_cs, tune_nuisance_did_cs +from ._utils_did_manual import boot_did @pytest.fixture(scope='module', diff --git a/doubleml/did/tests/test_did_external_predictions.py b/doubleml/did/tests/test_did_external_predictions.py index 676302e96..99888546d 100644 --- a/doubleml/did/tests/test_did_external_predictions.py +++ b/doubleml/did/tests/test_did_external_predictions.py @@ -1,10 +1,13 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LinearRegression, LogisticRegression + from doubleml import DoubleMLDID from doubleml.datasets import make_did_SZ2020 -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor + from ...tests._utils import draw_smpls diff --git a/doubleml/did/tests/test_did_tune.py b/doubleml/did/tests/test_did_tune.py index da85f6934..eb2324c20 100644 --- a/doubleml/did/tests/test_did_tune.py +++ b/doubleml/did/tests/test_did_tune.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_did_manual import fit_did, boot_did, tune_nuisance_did +from ._utils_did_manual import boot_did, fit_did, tune_nuisance_did @pytest.fixture(scope='module', From 030eeae0076135ceefbe295852e84146033913ed Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 10:54:17 +0100 Subject: [PATCH 05/28] reformat did dir --- doubleml/did/did.py | 360 +++++----- doubleml/did/did_cs.py | 647 ++++++++++-------- doubleml/did/tests/_utils_did_cs_manual.py | 453 +++++++----- doubleml/did/tests/_utils_did_manual.py | 170 ++--- doubleml/did/tests/conftest.py | 14 +- doubleml/did/tests/test_did.py | 176 ++--- doubleml/did/tests/test_did_cs.py | 180 ++--- .../tests/test_did_cs_external_predictions.py | 8 +- doubleml/did/tests/test_did_cs_tune.py | 138 ++-- .../tests/test_did_external_predictions.py | 7 +- doubleml/did/tests/test_did_tune.py | 131 ++-- 11 files changed, 1290 insertions(+), 994 deletions(-) diff --git a/doubleml/did/did.py b/doubleml/did/did.py index ed61ace69..495f63c8a 100644 --- a/doubleml/did/did.py +++ b/doubleml/did/did.py @@ -77,31 +77,32 @@ class DoubleMLDID(LinearScoreMixin, DoubleML): coef std err t P>|t| 2.5 % 97.5 % d -2.685104 1.798071 -1.493325 0.135352 -6.209257 0.83905 """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m=None, - n_folds=5, - n_rep=1, - score='observational', - in_sample_normalization=True, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m=None, + n_folds=5, + n_rep=1, + score="observational", + in_sample_normalization=True, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) - valid_scores = ['observational', 'experimental'] + valid_scores = ["observational", "experimental"] _check_score(self.score, valid_scores, allow_callable=False) self._in_sample_normalization = in_sample_normalization if not isinstance(self.in_sample_normalization, bool): - raise TypeError('in_sample_normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.in_sample_normalization))} passed.') + raise TypeError( + "in_sample_normalization indicator has to be boolean. " + + f"Object of type {str(type(self.in_sample_normalization))} passed." + ) # set stratication for resampling self._strata = self._dml_data.d @@ -109,28 +110,34 @@ def __init__(self, self.draw_sample_splitting() # check learners - ml_g_is_classifier = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - if self.score == 'observational': - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - self._learner = {'ml_g': ml_g, 'ml_m': ml_m} + ml_g_is_classifier = self._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + if self.score == "observational": + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + self._learner = {"ml_g": ml_g, "ml_m": ml_m} else: - assert self.score == 'experimental' + assert self.score == "experimental" if ml_m is not None: - warnings.warn(('A learner ml_m has been provided for score = "experimental" but will be ignored. ' - 'A learner ml_m is not required for estimation.')) - self._learner = {'ml_g': ml_g} + warnings.warn( + ( + 'A learner ml_m has been provided for score = "experimental" but will be ignored. ' + "A learner ml_m is not required for estimation." + ) + ) + self._learner = {"ml_g": ml_g} if ml_g_is_classifier: if obj_dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict'} + self._predict_method = {"ml_g": "predict"} - if 'ml_m' in self._learner: - self._predict_method['ml_m'] = 'predict_proba' + if "ml_m" in self._learner: + self._predict_method["ml_m"] = "predict_proba" self._initialize_ml_nuisance_params() self._trimming_rule = trimming_rule @@ -161,109 +168,118 @@ def trimming_threshold(self): return self._trimming_threshold def _initialize_ml_nuisance_params(self): - if self.score == 'observational': - valid_learner = ['ml_g0', 'ml_g1', 'ml_m'] + if self.score == "observational": + valid_learner = ["ml_g0", "ml_g1", "ml_m"] else: - assert self.score == 'experimental' - valid_learner = ['ml_g0', 'ml_g1'] - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in valid_learner} + assert self.score == "experimental" + valid_learner = ["ml_g0", "ml_g1"] + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('For repeated outcomes the data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "For repeated outcomes the data must be of DoubleMLData type. " + f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ' - 'At the moment there are not DiD models with instruments implemented.') - one_treat = (obj_dml_data.n_treat == 1) - binary_treat = (type_of_target(obj_dml_data.d) == 'binary') + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + "At the moment there are not DiD models with instruments implemented." + ) + one_treat = obj_dml_data.n_treat == 1 + binary_treat = type_of_target(obj_dml_data.d) == "binary" zero_one_treat = np.all((np.power(obj_dml_data.d, 2) - obj_dml_data.d) == 0) if not (one_treat & binary_treat & zero_one_treat): - raise ValueError('Incompatible data. ' - 'To fit an DID model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + raise ValueError( + "Incompatible data. " + "To fit an DID model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # nuisance g # get train indices for d == 0 smpls_d0, smpls_d1 = _get_cond_smpls(smpls, d) # nuisance g for d==0 - if external_predictions['ml_g0'] is not None: - g_hat0 = {'preds': external_predictions['ml_g0'], - 'targets': None, - 'models': None} + if external_predictions["ml_g0"] is not None: + g_hat0 = {"preds": external_predictions["ml_g0"], "targets": None, "models": None} else: - g_hat0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g0'), method=self._predict_method['ml_g'], - return_models=return_models) - - _check_finite_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', smpls) + g_hat0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + + _check_finite_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", smpls) # adjust target values to consider only compatible subsamples - g_hat0['targets'] = g_hat0['targets'].astype(float) - g_hat0['targets'][d == 1] = np.nan + g_hat0["targets"] = g_hat0["targets"].astype(float) + g_hat0["targets"][d == 1] = np.nan # nuisance g for d==1 - if external_predictions['ml_g1'] is not None: - g_hat1 = {'preds': external_predictions['ml_g1'], - 'targets': None, - 'models': None} + if external_predictions["ml_g1"] is not None: + g_hat1 = {"preds": external_predictions["ml_g1"], "targets": None, "models": None} else: - g_hat1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g1'), method=self._predict_method['ml_g'], - return_models=return_models) - - _check_finite_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', smpls) + g_hat1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + + _check_finite_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", smpls) # adjust target values to consider only compatible subsamples - g_hat1['targets'] = g_hat1['targets'].astype(float) - g_hat1['targets'][d == 0] = np.nan + g_hat1["targets"] = g_hat1["targets"].astype(float) + g_hat1["targets"][d == 0] = np.nan # only relevant for observational setting - m_hat = {'preds': None, 'targets': None, 'models': None} - if self.score == 'observational': + m_hat = {"preds": None, "targets": None, "models": None} + if self.score == "observational": # nuisance m - if external_predictions['ml_m'] is not None: - m_hat = {'preds': external_predictions['ml_m'], - 'targets': None, - 'models': None} + if external_predictions["ml_m"] is not None: + m_hat = {"preds": external_predictions["ml_m"], "targets": None, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) - _check_is_propensity(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls, eps=1e-12) - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) + _check_is_propensity(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls, eps=1e-12) + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) # nuisance estimates of the uncond. treatment prob. - p_hat = np.full_like(d, np.nan, dtype='float64') + p_hat = np.full_like(d, np.nan, dtype="float64") for train_index, test_index in smpls: p_hat[test_index] = np.mean(d[train_index]) - psi_a, psi_b = self._score_elements(y, d, g_hat0['preds'], g_hat1['preds'], m_hat['preds'], p_hat) - - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_g0': g_hat0['preds'], - 'ml_g1': g_hat1['preds'], - 'ml_m': m_hat['preds']}, - 'targets': {'ml_g0': g_hat0['targets'], - 'ml_g1': g_hat1['targets'], - 'ml_m': m_hat['targets']}, - 'models': {'ml_g0': g_hat0['models'], - 'ml_g1': g_hat1['models'], - 'ml_m': m_hat['models'] - } - } + psi_a, psi_b = self._score_elements(y, d, g_hat0["preds"], g_hat1["preds"], m_hat["preds"], p_hat) + + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_g0": g_hat0["preds"], "ml_g1": g_hat1["preds"], "ml_m": m_hat["preds"]}, + "targets": {"ml_g0": g_hat0["targets"], "ml_g1": g_hat1["targets"], "ml_m": m_hat["targets"]}, + "models": {"ml_g0": g_hat0["models"], "ml_g1": g_hat1["models"], "ml_m": m_hat["models"]}, + } return psi_elements, preds @@ -271,35 +287,35 @@ def _score_elements(self, y, d, g_hat0, g_hat1, m_hat, p_hat): # calc residuals resid_d0 = y - g_hat0 - if self.score == 'observational': + if self.score == "observational": if self.in_sample_normalization: weight_psi_a = np.divide(d, np.mean(d)) - propensity_weight = np.multiply(1.0-d, np.divide(m_hat, 1.0-m_hat)) + propensity_weight = np.multiply(1.0 - d, np.divide(m_hat, 1.0 - m_hat)) weight_resid_d0 = np.divide(d, np.mean(d)) - np.divide(propensity_weight, np.mean(propensity_weight)) else: weight_psi_a = np.divide(d, p_hat) - weight_resid_d0 = np.divide(d-m_hat, np.multiply(p_hat, 1.0-m_hat)) + weight_resid_d0 = np.divide(d - m_hat, np.multiply(p_hat, 1.0 - m_hat)) psi_b_1 = np.zeros_like(y) else: - assert self.score == 'experimental' + assert self.score == "experimental" if self.in_sample_normalization: weight_psi_a = np.ones_like(y) weight_g0 = np.divide(d, np.mean(d)) - 1.0 weight_g1 = 1.0 - np.divide(d, np.mean(d)) - weight_resid_d0 = np.divide(d, np.mean(d)) - np.divide(1.0-d, np.mean(1.0-d)) + weight_resid_d0 = np.divide(d, np.mean(d)) - np.divide(1.0 - d, np.mean(1.0 - d)) else: weight_psi_a = np.ones_like(y) weight_g0 = np.divide(d, p_hat) - 1.0 weight_g1 = 1.0 - np.divide(d, p_hat) - weight_resid_d0 = np.divide(d-p_hat, np.multiply(p_hat, 1.0-p_hat)) + weight_resid_d0 = np.divide(d - p_hat, np.multiply(p_hat, 1.0 - p_hat)) - psi_b_1 = np.multiply(weight_g0, g_hat0) + np.multiply(weight_g1, g_hat1) + psi_b_1 = np.multiply(weight_g0, g_hat0) + np.multiply(weight_g1, g_hat1) # set score elements psi_a = -1.0 * weight_psi_a - psi_b = psi_b_1 + np.multiply(weight_resid_d0, resid_d0) + psi_b = psi_b_1 + np.multiply(weight_resid_d0, resid_d0) return psi_a, psi_b @@ -307,92 +323,112 @@ def _sensitivity_element_est(self, preds): y = self._dml_data.y d = self._dml_data.d - m_hat = preds['predictions']['ml_m'] - g_hat0 = preds['predictions']['ml_g0'] - g_hat1 = preds['predictions']['ml_g1'] + m_hat = preds["predictions"]["ml_m"] + g_hat0 = preds["predictions"]["ml_g0"] + g_hat1 = preds["predictions"]["ml_g1"] - g_hat = np.multiply(d, g_hat1) + np.multiply(1.0-d, g_hat0) + g_hat = np.multiply(d, g_hat1) + np.multiply(1.0 - d, g_hat0) sigma2_score_element = np.square(y - g_hat) sigma2 = np.mean(sigma2_score_element) psi_sigma2 = sigma2_score_element - sigma2 # calc m(W,alpha) and Riesz representer p_hat = np.mean(d) - if self.score == 'observational': - propensity_weight_d0 = np.divide(m_hat, 1.0-m_hat) + if self.score == "observational": + propensity_weight_d0 = np.divide(m_hat, 1.0 - m_hat) if self.in_sample_normalization: - weight_d0 = np.multiply(1.0-d, propensity_weight_d0) + weight_d0 = np.multiply(1.0 - d, propensity_weight_d0) mean_weight_d0 = np.mean(weight_d0) - m_alpha = np.multiply(np.divide(d, p_hat), - np.divide(1.0, p_hat) + np.divide(propensity_weight_d0, mean_weight_d0)) + m_alpha = np.multiply( + np.divide(d, p_hat), np.divide(1.0, p_hat) + np.divide(propensity_weight_d0, mean_weight_d0) + ) rr = np.divide(d, p_hat) - np.divide(weight_d0, mean_weight_d0) else: m_alpha = np.multiply(np.divide(d, np.square(p_hat)), (1.0 + propensity_weight_d0)) - rr = np.divide(d, p_hat) - np.multiply(np.divide(1.0-d, p_hat), propensity_weight_d0) + rr = np.divide(d, p_hat) - np.multiply(np.divide(1.0 - d, p_hat), propensity_weight_d0) else: - assert self.score == 'experimental' + assert self.score == "experimental" # the same with or without self-normalization - m_alpha = np.divide(1.0, p_hat) + np.divide(1.0, 1.0-p_hat) - rr = np.divide(d, p_hat) - np.divide(1.0-d, 1.0-p_hat) + m_alpha = np.divide(1.0, p_hat) + np.divide(1.0, 1.0 - p_hat) + rr = np.divide(d, p_hat) - np.divide(1.0 - d, 1.0 - p_hat) nu2_score_element = np.multiply(2.0, m_alpha) - np.square(rr) nu2 = np.mean(nu2_score_element) psi_nu2 = nu2_score_element - nu2 - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2, - 'riesz_rep': rr, - } + element_dict = { + "sigma2": sigma2, + "nu2": nu2, + "psi_sigma2": psi_sigma2, + "psi_nu2": psi_nu2, + "riesz_rep": rr, + } return element_dict - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # get train indices for d == 0 and d == 1 smpls_d0, smpls_d1 = _get_cond_smpls(smpls, d) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': None} + scoring_methods = {"ml_g": None, "ml_m": None} train_inds = [train_index for (train_index, _) in smpls] train_inds_d0 = [train_index for (train_index, _) in smpls_d0] train_inds_d1 = [train_index for (train_index, _) in smpls_d1] - g0_tune_res = _dml_tune(y, x, train_inds_d0, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - g1_tune_res = _dml_tune(y, x, train_inds_d1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g0_tune_res = _dml_tune( + y, + x, + train_inds_d0, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + g1_tune_res = _dml_tune( + y, + x, + train_inds_d1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g0_best_params = [xx.best_params_ for xx in g0_tune_res] g1_best_params = [xx.best_params_ for xx in g1_tune_res] m_tune_res = list() - if self.score == 'observational': - m_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + if self.score == "observational": + m_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g0': g0_best_params, - 'ml_g1': g1_best_params, - 'ml_m': m_best_params} - tune_res = {'g0_tune': g0_tune_res, - 'g1_tune': g1_tune_res, - 'm_tune': m_tune_res} + params = {"ml_g0": g0_best_params, "ml_g1": g1_best_params, "ml_m": m_best_params} + tune_res = {"g0_tune": g0_tune_res, "g1_tune": g1_tune_res, "m_tune": m_tune_res} else: - params = {'ml_g0': g0_best_params, - 'ml_g1': g1_best_params} - tune_res = {'g0_tune': g0_tune_res, - 'g1_tune': g1_tune_res} + params = {"ml_g0": g0_best_params, "ml_g1": g1_best_params} + tune_res = {"g0_tune": g0_tune_res, "g1_tune": g1_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res diff --git a/doubleml/did/did_cs.py b/doubleml/did/did_cs.py index a5c9780b3..f223cccf5 100644 --- a/doubleml/did/did_cs.py +++ b/doubleml/did/did_cs.py @@ -77,31 +77,32 @@ class DoubleMLDIDCS(LinearScoreMixin, DoubleML): coef std err t P>|t| 2.5 % 97.5 % d -6.604603 8.725802 -0.756905 0.449107 -23.706862 10.497655 """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m=None, - n_folds=5, - n_rep=1, - score='observational', - in_sample_normalization=True, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m=None, + n_folds=5, + n_rep=1, + score="observational", + in_sample_normalization=True, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) - valid_scores = ['observational', 'experimental'] + valid_scores = ["observational", "experimental"] _check_score(self.score, valid_scores, allow_callable=False) self._in_sample_normalization = in_sample_normalization if not isinstance(self.in_sample_normalization, bool): - raise TypeError('in_sample_normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.in_sample_normalization))} passed.') + raise TypeError( + "in_sample_normalization indicator has to be boolean. " + + f"Object of type {str(type(self.in_sample_normalization))} passed." + ) # set stratication for resampling self._strata = self._dml_data.d.reshape(-1, 1) + 2 * self._dml_data.t.reshape(-1, 1) @@ -109,28 +110,34 @@ def __init__(self, self.draw_sample_splitting() # check learners - ml_g_is_classifier = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - if self.score == 'observational': - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - self._learner = {'ml_g': ml_g, 'ml_m': ml_m} + ml_g_is_classifier = self._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + if self.score == "observational": + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + self._learner = {"ml_g": ml_g, "ml_m": ml_m} else: - assert self.score == 'experimental' + assert self.score == "experimental" if ml_m is not None: - warnings.warn(('A learner ml_m has been provided for score = "experimental" but will be ignored. ' - 'A learner ml_m is not required for estimation.')) - self._learner = {'ml_g': ml_g} + warnings.warn( + ( + 'A learner ml_m has been provided for score = "experimental" but will be ignored. ' + "A learner ml_m is not required for estimation." + ) + ) + self._learner = {"ml_g": ml_g} if ml_g_is_classifier: if obj_dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict'} + self._predict_method = {"ml_g": "predict"} - if 'ml_m' in self._learner: - self._predict_method['ml_m'] = 'predict_proba' + if "ml_m" in self._learner: + self._predict_method["ml_m"] = "predict_proba" self._initialize_ml_nuisance_params() self._trimming_rule = trimming_rule @@ -162,156 +169,190 @@ def trimming_threshold(self): return self._trimming_threshold def _initialize_ml_nuisance_params(self): - if self.score == 'observational': - valid_learner = ['ml_g_d0_t0', 'ml_g_d0_t1', - 'ml_g_d1_t0', 'ml_g_d1_t1', 'ml_m'] + if self.score == "observational": + valid_learner = ["ml_g_d0_t0", "ml_g_d0_t1", "ml_g_d1_t0", "ml_g_d1_t1", "ml_m"] else: - assert self.score == 'experimental' - valid_learner = ['ml_g_d0_t0', 'ml_g_d0_t1', - 'ml_g_d1_t0', 'ml_g_d1_t1'] - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in valid_learner} + assert self.score == "experimental" + valid_learner = ["ml_g_d0_t0", "ml_g_d0_t1", "ml_g_d1_t0", "ml_g_d1_t1"] + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('For repeated cross sections the data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "For repeated cross sections the data must be of DoubleMLData type. " + f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ' - 'At the moment there are no DiD models with instruments implemented.') - one_treat = (obj_dml_data.n_treat == 1) - binary_treat = (type_of_target(obj_dml_data.d) == 'binary') - zero_one_treat = np.all( - (np.power(obj_dml_data.d, 2) - obj_dml_data.d) == 0) + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + "At the moment there are no DiD models with instruments implemented." + ) + one_treat = obj_dml_data.n_treat == 1 + binary_treat = type_of_target(obj_dml_data.d) == "binary" + zero_one_treat = np.all((np.power(obj_dml_data.d, 2) - obj_dml_data.d) == 0) if not (one_treat & binary_treat & zero_one_treat): - raise ValueError('Incompatible data. ' - 'To fit an DIDCS model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + raise ValueError( + "Incompatible data. " + "To fit an DIDCS model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) - binary_time = (type_of_target(obj_dml_data.t) == 'binary') - zero_one_time = np.all( - (np.power(obj_dml_data.t, 2) - obj_dml_data.t) == 0) + binary_time = type_of_target(obj_dml_data.t) == "binary" + zero_one_time = np.all((np.power(obj_dml_data.t, 2) - obj_dml_data.t) == 0) if not (binary_time & zero_one_time): - raise ValueError('Incompatible data. ' - 'To fit an DIDCS model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as time variable.') + raise ValueError( + "Incompatible data. " + "To fit an DIDCS model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as time variable." + ) return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) - x, t = check_X_y(x, self._dml_data.t, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) + x, t = check_X_y(x, self._dml_data.t, force_all_finite=False) # THIS DIFFERS FROM THE PAPER due to stratified splitting this should be the same for each fold # nuisance estimates of the uncond. treatment prob. - p_hat = np.full_like(d, np.nan, dtype='float64') + p_hat = np.full_like(d, np.nan, dtype="float64") for train_index, test_index in smpls: p_hat[test_index] = np.mean(d[train_index]) # nuisance estimates of the uncond. time prob. - lambda_hat = np.full_like(t, np.nan, dtype='float64') + lambda_hat = np.full_like(t, np.nan, dtype="float64") for train_index, test_index in smpls: lambda_hat[test_index] = np.mean(t[train_index]) # nuisance g smpls_d0_t0, smpls_d0_t1, smpls_d1_t0, smpls_d1_t1 = _get_cond_smpls_2d(smpls, d, t) - if external_predictions['ml_g_d0_t0'] is not None: - g_hat_d0_t0 = {'preds': external_predictions['ml_g_d0_t0'], - 'targets': None, - 'models': None} + if external_predictions["ml_g_d0_t0"] is not None: + g_hat_d0_t0 = {"preds": external_predictions["ml_g_d0_t0"], "targets": None, "models": None} else: - g_hat_d0_t0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls_d0_t0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g_d0_t0'), method=self._predict_method['ml_g'], - return_models=return_models) - - g_hat_d0_t0['targets'] = g_hat_d0_t0['targets'].astype(float) - g_hat_d0_t0['targets'][np.invert((d == 0) & (t == 0))] = np.nan - if external_predictions['ml_g_d0_t1'] is not None: - g_hat_d0_t1 = {'preds': external_predictions['ml_g_d0_t1'], - 'targets': None, - 'models': None} + g_hat_d0_t0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls_d0_t0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g_d0_t0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + + g_hat_d0_t0["targets"] = g_hat_d0_t0["targets"].astype(float) + g_hat_d0_t0["targets"][np.invert((d == 0) & (t == 0))] = np.nan + if external_predictions["ml_g_d0_t1"] is not None: + g_hat_d0_t1 = {"preds": external_predictions["ml_g_d0_t1"], "targets": None, "models": None} else: - g_hat_d0_t1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls_d0_t1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g_d0_t1'), method=self._predict_method['ml_g'], - return_models=return_models) - g_hat_d0_t1['targets'] = g_hat_d0_t1['targets'].astype(float) - g_hat_d0_t1['targets'][np.invert((d == 0) & (t == 1))] = np.nan - if external_predictions['ml_g_d1_t0'] is not None: - g_hat_d1_t0 = {'preds': external_predictions['ml_g_d1_t0'], - 'targets': None, - 'models': None} + g_hat_d0_t1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls_d0_t1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g_d0_t1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + g_hat_d0_t1["targets"] = g_hat_d0_t1["targets"].astype(float) + g_hat_d0_t1["targets"][np.invert((d == 0) & (t == 1))] = np.nan + if external_predictions["ml_g_d1_t0"] is not None: + g_hat_d1_t0 = {"preds": external_predictions["ml_g_d1_t0"], "targets": None, "models": None} else: - g_hat_d1_t0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls_d1_t0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g_d1_t0'), method=self._predict_method['ml_g'], - return_models=return_models) - g_hat_d1_t0['targets'] = g_hat_d1_t0['targets'].astype(float) - g_hat_d1_t0['targets'][np.invert((d == 1) & (t == 0))] = np.nan - if external_predictions['ml_g_d1_t1'] is not None: - g_hat_d1_t1 = {'preds': external_predictions['ml_g_d1_t1'], - 'targets': None, - 'models': None} + g_hat_d1_t0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls_d1_t0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g_d1_t0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + g_hat_d1_t0["targets"] = g_hat_d1_t0["targets"].astype(float) + g_hat_d1_t0["targets"][np.invert((d == 1) & (t == 0))] = np.nan + if external_predictions["ml_g_d1_t1"] is not None: + g_hat_d1_t1 = {"preds": external_predictions["ml_g_d1_t1"], "targets": None, "models": None} else: - g_hat_d1_t1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls_d1_t1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g_d1_t1'), method=self._predict_method['ml_g'], - return_models=return_models) - g_hat_d1_t1['targets'] = g_hat_d1_t1['targets'].astype(float) - g_hat_d1_t1['targets'][np.invert((d == 1) & (t == 1))] = np.nan + g_hat_d1_t1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls_d1_t1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g_d1_t1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + g_hat_d1_t1["targets"] = g_hat_d1_t1["targets"].astype(float) + g_hat_d1_t1["targets"][np.invert((d == 1) & (t == 1))] = np.nan # only relevant for observational or experimental setting - m_hat = {'preds': None, 'targets': None, 'models': None} - if self.score == 'observational': + m_hat = {"preds": None, "targets": None, "models": None} + if self.score == "observational": # nuisance m - if external_predictions['ml_m'] is not None: - m_hat = {'preds': external_predictions['ml_m'], - 'targets': None, - 'models': None} + if external_predictions["ml_m"] is not None: + m_hat = {"preds": external_predictions["ml_m"], "targets": None, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) - _check_is_propensity(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls, eps=1e-12) - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) - - psi_a, psi_b = self._score_elements(y, d, t, - g_hat_d0_t0['preds'], g_hat_d0_t1['preds'], - g_hat_d1_t0['preds'], g_hat_d1_t1['preds'], - m_hat['preds'], p_hat, lambda_hat) - - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_g_d0_t0': g_hat_d0_t0['preds'], - 'ml_g_d0_t1': g_hat_d0_t1['preds'], - 'ml_g_d1_t0': g_hat_d1_t0['preds'], - 'ml_g_d1_t1': g_hat_d1_t1['preds'], - 'ml_m': m_hat['preds']}, - 'targets': {'ml_g_d0_t0': g_hat_d0_t0['targets'], - 'ml_g_d0_t1': g_hat_d0_t1['targets'], - 'ml_g_d1_t0': g_hat_d1_t0['targets'], - 'ml_g_d1_t1': g_hat_d1_t1['targets'], - 'ml_m': m_hat['targets']}, - 'models': {'ml_g_d0_t0': g_hat_d0_t0['models'], - 'ml_g_d0_t1': g_hat_d0_t1['models'], - 'ml_g_d1_t0': g_hat_d1_t0['models'], - 'ml_g_d1_t1': g_hat_d1_t1['models'], - 'ml_m': m_hat['models']} - } + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) + _check_is_propensity(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls, eps=1e-12) + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) + + psi_a, psi_b = self._score_elements( + y, + d, + t, + g_hat_d0_t0["preds"], + g_hat_d0_t1["preds"], + g_hat_d1_t0["preds"], + g_hat_d1_t1["preds"], + m_hat["preds"], + p_hat, + lambda_hat, + ) + + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": { + "ml_g_d0_t0": g_hat_d0_t0["preds"], + "ml_g_d0_t1": g_hat_d0_t1["preds"], + "ml_g_d1_t0": g_hat_d1_t0["preds"], + "ml_g_d1_t1": g_hat_d1_t1["preds"], + "ml_m": m_hat["preds"], + }, + "targets": { + "ml_g_d0_t0": g_hat_d0_t0["targets"], + "ml_g_d0_t1": g_hat_d0_t1["targets"], + "ml_g_d1_t0": g_hat_d1_t0["targets"], + "ml_g_d1_t1": g_hat_d1_t1["targets"], + "ml_m": m_hat["targets"], + }, + "models": { + "ml_g_d0_t0": g_hat_d0_t0["models"], + "ml_g_d0_t1": g_hat_d0_t1["models"], + "ml_g_d1_t0": g_hat_d1_t0["models"], + "ml_g_d1_t1": g_hat_d1_t1["models"], + "ml_m": m_hat["models"], + }, + } return psi_elements, preds - def _score_elements(self, y, d, t, - g_hat_d0_t0, g_hat_d0_t1, - g_hat_d1_t0, g_hat_d1_t1, - m_hat, p_hat, lambda_hat): + def _score_elements(self, y, d, t, g_hat_d0_t0, g_hat_d0_t1, g_hat_d1_t0, g_hat_d1_t1, m_hat, p_hat, lambda_hat): # calculate residuals resid_d0_t0 = y - g_hat_d0_t0 @@ -320,11 +361,11 @@ def _score_elements(self, y, d, t, resid_d1_t1 = y - g_hat_d1_t1 d1t1 = np.multiply(d, t) - d1t0 = np.multiply(d, 1.0-t) - d0t1 = np.multiply(1.0-d, t) - d0t0 = np.multiply(1.0-d, 1.0-t) + d1t0 = np.multiply(d, 1.0 - t) + d0t1 = np.multiply(1.0 - d, t) + d0t0 = np.multiply(1.0 - d, 1.0 - t) - if self.score == 'observational': + if self.score == "observational": if self.in_sample_normalization: weight_psi_a = np.divide(d, np.mean(d)) weight_g_d1_t1 = weight_psi_a @@ -335,7 +376,7 @@ def _score_elements(self, y, d, t, weight_resid_d1_t1 = np.divide(d1t1, np.mean(d1t1)) weight_resid_d1_t0 = -1.0 * np.divide(d1t0, np.mean(d1t0)) - prop_weighting = np.divide(m_hat, 1.0-m_hat) + prop_weighting = np.divide(m_hat, 1.0 - m_hat) unscaled_d0_t1 = np.multiply(d0t1, prop_weighting) weight_resid_d0_t1 = -1.0 * np.divide(unscaled_d0_t1, np.mean(unscaled_d0_t1)) @@ -349,15 +390,13 @@ def _score_elements(self, y, d, t, weight_g_d0_t0 = weight_psi_a weight_resid_d1_t1 = np.divide(d1t1, np.multiply(p_hat, lambda_hat)) - weight_resid_d1_t0 = -1.0 * np.divide(d1t0, np.multiply(p_hat, 1.0-lambda_hat)) + weight_resid_d1_t0 = -1.0 * np.divide(d1t0, np.multiply(p_hat, 1.0 - lambda_hat)) - prop_weighting = np.divide(m_hat, 1.0-m_hat) - weight_resid_d0_t1 = -1.0 * np.multiply(np.divide(d0t1, np.multiply(p_hat, lambda_hat)), - prop_weighting) - weight_resid_d0_t0 = np.multiply(np.divide(d0t0, np.multiply(p_hat, 1.0-lambda_hat)), - prop_weighting) + prop_weighting = np.divide(m_hat, 1.0 - m_hat) + weight_resid_d0_t1 = -1.0 * np.multiply(np.divide(d0t1, np.multiply(p_hat, lambda_hat)), prop_weighting) + weight_resid_d0_t0 = np.multiply(np.divide(d0t0, np.multiply(p_hat, 1.0 - lambda_hat)), prop_weighting) else: - assert self.score == 'experimental' + assert self.score == "experimental" if self.in_sample_normalization: weight_psi_a = np.ones_like(y) weight_g_d1_t1 = weight_psi_a @@ -377,22 +416,26 @@ def _score_elements(self, y, d, t, weight_g_d0_t0 = weight_psi_a weight_resid_d1_t1 = np.divide(d1t1, np.multiply(p_hat, lambda_hat)) - weight_resid_d1_t0 = -1.0 * np.divide(d1t0, np.multiply(p_hat, 1.0-lambda_hat)) - weight_resid_d0_t1 = -1.0 * np.divide(d0t1, np.multiply(1.0-p_hat, lambda_hat)) - weight_resid_d0_t0 = np.divide(d0t0, np.multiply(1.0-p_hat, 1.0-lambda_hat)) + weight_resid_d1_t0 = -1.0 * np.divide(d1t0, np.multiply(p_hat, 1.0 - lambda_hat)) + weight_resid_d0_t1 = -1.0 * np.divide(d0t1, np.multiply(1.0 - p_hat, lambda_hat)) + weight_resid_d0_t0 = np.divide(d0t0, np.multiply(1.0 - p_hat, 1.0 - lambda_hat)) # set score elements psi_a = -1.0 * weight_psi_a # psi_b - psi_b_1 = np.multiply(weight_g_d1_t1, g_hat_d1_t1) + \ - np.multiply(weight_g_d1_t0, g_hat_d1_t0) + \ - np.multiply(weight_g_d0_t0, g_hat_d0_t0) + \ - np.multiply(weight_g_d0_t1, g_hat_d0_t1) - psi_b_2 = np.multiply(weight_resid_d1_t1, resid_d1_t1) + \ - np.multiply(weight_resid_d1_t0, resid_d1_t0) + \ - np.multiply(weight_resid_d0_t0, resid_d0_t0) + \ - np.multiply(weight_resid_d0_t1, resid_d0_t1) + psi_b_1 = ( + np.multiply(weight_g_d1_t1, g_hat_d1_t1) + + np.multiply(weight_g_d1_t0, g_hat_d1_t0) + + np.multiply(weight_g_d0_t0, g_hat_d0_t0) + + np.multiply(weight_g_d0_t1, g_hat_d0_t1) + ) + psi_b_2 = ( + np.multiply(weight_resid_d1_t1, resid_d1_t1) + + np.multiply(weight_resid_d1_t0, resid_d1_t0) + + np.multiply(weight_resid_d0_t0, resid_d0_t0) + + np.multiply(weight_resid_d0_t1, resid_d0_t1) + ) psi_b = psi_b_1 + psi_b_2 @@ -403,19 +446,23 @@ def _sensitivity_element_est(self, preds): d = self._dml_data.d t = self._dml_data.t - m_hat = preds['predictions']['ml_m'] - g_hat_d0_t0 = preds['predictions']['ml_g_d0_t0'] - g_hat_d0_t1 = preds['predictions']['ml_g_d0_t1'] - g_hat_d1_t0 = preds['predictions']['ml_g_d1_t0'] - g_hat_d1_t1 = preds['predictions']['ml_g_d1_t1'] + m_hat = preds["predictions"]["ml_m"] + g_hat_d0_t0 = preds["predictions"]["ml_g_d0_t0"] + g_hat_d0_t1 = preds["predictions"]["ml_g_d0_t1"] + g_hat_d1_t0 = preds["predictions"]["ml_g_d1_t0"] + g_hat_d1_t1 = preds["predictions"]["ml_g_d1_t1"] - d0t0 = np.multiply(1.0-d, 1.0-t) - d0t1 = np.multiply(1.0-d, t) - d1t0 = np.multiply(d, 1.0-t) + d0t0 = np.multiply(1.0 - d, 1.0 - t) + d0t1 = np.multiply(1.0 - d, t) + d1t0 = np.multiply(d, 1.0 - t) d1t1 = np.multiply(d, t) - g_hat = np.multiply(d0t0, g_hat_d0_t0) + np.multiply(d0t1, g_hat_d0_t1) + \ - np.multiply(d1t0, g_hat_d1_t0) + np.multiply(d1t1, g_hat_d1_t1) + g_hat = ( + np.multiply(d0t0, g_hat_d0_t0) + + np.multiply(d0t1, g_hat_d0_t1) + + np.multiply(d1t0, g_hat_d1_t0) + + np.multiply(d1t1, g_hat_d1_t1) + ) sigma2_score_element = np.square(y - g_hat) sigma2 = np.mean(sigma2_score_element) psi_sigma2 = sigma2_score_element - sigma2 @@ -423,76 +470,86 @@ def _sensitivity_element_est(self, preds): # calc m(W,alpha) and Riesz representer p_hat = np.mean(d) lambda_hat = np.mean(t) - if self.score == 'observational': - propensity_weight_d0 = np.divide(m_hat, 1.0-m_hat) + if self.score == "observational": + propensity_weight_d0 = np.divide(m_hat, 1.0 - m_hat) if self.in_sample_normalization: weight_d0t1 = np.multiply(d0t1, propensity_weight_d0) weight_d0t0 = np.multiply(d0t0, propensity_weight_d0) mean_weight_d0t1 = np.mean(weight_d0t1) mean_weight_d0t0 = np.mean(weight_d0t0) - m_alpha = np.multiply(np.divide(d, p_hat), - np.divide(1.0, np.mean(d1t1)) + - np.divide(1.0, np.mean(d1t0)) + - np.divide(propensity_weight_d0, mean_weight_d0t1) + - np.divide(propensity_weight_d0, mean_weight_d0t0)) - - rr = np.divide(d1t1, np.mean(d1t1)) - \ - np.divide(d1t0, np.mean(d1t0)) - \ - np.divide(weight_d0t1, mean_weight_d0t1) + \ - np.divide(weight_d0t0, mean_weight_d0t0) + m_alpha = np.multiply( + np.divide(d, p_hat), + np.divide(1.0, np.mean(d1t1)) + + np.divide(1.0, np.mean(d1t0)) + + np.divide(propensity_weight_d0, mean_weight_d0t1) + + np.divide(propensity_weight_d0, mean_weight_d0t0), + ) + + rr = ( + np.divide(d1t1, np.mean(d1t1)) + - np.divide(d1t0, np.mean(d1t0)) + - np.divide(weight_d0t1, mean_weight_d0t1) + + np.divide(weight_d0t0, mean_weight_d0t0) + ) else: - m_alpha_1 = np.divide(1.0, lambda_hat) + np.divide(1.0, 1.0-lambda_hat) + m_alpha_1 = np.divide(1.0, lambda_hat) + np.divide(1.0, 1.0 - lambda_hat) m_alpha = np.multiply(np.divide(d, np.square(p_hat)), np.multiply(m_alpha_1, 1.0 + propensity_weight_d0)) - rr_1 = np.divide(t, np.multiply(p_hat, lambda_hat)) + np.divide(1.0-t, np.multiply(p_hat, 1.0-lambda_hat)) - rr_2 = d + np.multiply(1.0-d, propensity_weight_d0) + rr_1 = np.divide(t, np.multiply(p_hat, lambda_hat)) + np.divide(1.0 - t, np.multiply(p_hat, 1.0 - lambda_hat)) + rr_2 = d + np.multiply(1.0 - d, propensity_weight_d0) rr = np.multiply(rr_1, rr_2) else: - assert self.score == 'experimental' + assert self.score == "experimental" if self.in_sample_normalization: - m_alpha = np.divide(1.0, np.mean(d1t1)) + \ - np.divide(1.0, np.mean(d1t0)) + \ - np.divide(1.0, np.mean(d0t1)) + \ - np.divide(1.0, np.mean(d0t0)) - rr = np.divide(d1t1, np.mean(d1t1)) - \ - np.divide(d1t0, np.mean(d1t0)) - \ - np.divide(d0t1, np.mean(d0t1)) + \ - np.divide(d0t0, np.mean(d0t0)) + m_alpha = ( + np.divide(1.0, np.mean(d1t1)) + + np.divide(1.0, np.mean(d1t0)) + + np.divide(1.0, np.mean(d0t1)) + + np.divide(1.0, np.mean(d0t0)) + ) + rr = ( + np.divide(d1t1, np.mean(d1t1)) + - np.divide(d1t0, np.mean(d1t0)) + - np.divide(d0t1, np.mean(d0t1)) + + np.divide(d0t0, np.mean(d0t0)) + ) else: - m_alpha = np.divide(1.0, np.multiply(p_hat, lambda_hat)) + \ - np.divide(1.0, np.multiply(p_hat, 1.0-lambda_hat)) + \ - np.divide(1.0, np.multiply(1.0-p_hat, lambda_hat)) + \ - np.divide(1.0, np.multiply(1.0-p_hat, 1.0-lambda_hat)) - rr = np.divide(d1t1, np.multiply(p_hat, lambda_hat)) - \ - np.divide(d1t0, np.multiply(p_hat, 1.0-lambda_hat)) - \ - np.divide(d0t1, np.multiply(1.0-p_hat, lambda_hat)) + \ - np.divide(d0t0, np.multiply(1.0-p_hat, 1.0-lambda_hat)) + m_alpha = ( + np.divide(1.0, np.multiply(p_hat, lambda_hat)) + + np.divide(1.0, np.multiply(p_hat, 1.0 - lambda_hat)) + + np.divide(1.0, np.multiply(1.0 - p_hat, lambda_hat)) + + np.divide(1.0, np.multiply(1.0 - p_hat, 1.0 - lambda_hat)) + ) + rr = ( + np.divide(d1t1, np.multiply(p_hat, lambda_hat)) + - np.divide(d1t0, np.multiply(p_hat, 1.0 - lambda_hat)) + - np.divide(d0t1, np.multiply(1.0 - p_hat, lambda_hat)) + + np.divide(d0t0, np.multiply(1.0 - p_hat, 1.0 - lambda_hat)) + ) nu2_score_element = np.multiply(2.0, m_alpha) - np.square(rr) nu2 = np.mean(nu2_score_element) psi_nu2 = nu2_score_element - nu2 - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2, - 'riesz_rep': rr, - } + element_dict = { + "sigma2": sigma2, + "nu2": nu2, + "psi_sigma2": psi_sigma2, + "psi_nu2": psi_nu2, + "riesz_rep": rr, + } return element_dict - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) - x, t = check_X_y(x, self._dml_data.t, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) + x, t = check_X_y(x, self._dml_data.t, force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': None} + scoring_methods = {"ml_g": None, "ml_m": None} # nuisance training sets conditional on d and t smpls_d0_t0, smpls_d0_t1, smpls_d1_t0, smpls_d1_t1 = _get_cond_smpls_2d(smpls, d, t) @@ -502,56 +559,108 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ train_inds_d1_t0 = [train_index for (train_index, _) in smpls_d1_t0] train_inds_d1_t1 = [train_index for (train_index, _) in smpls_d1_t1] - g_d0_t0_tune_res = _dml_tune(y, x, train_inds_d0_t0, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - g_d0_t1_tune_res = _dml_tune(y, x, train_inds_d0_t1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - g_d1_t0_tune_res = _dml_tune(y, x, train_inds_d1_t0, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - g_d1_t1_tune_res = _dml_tune(y, x, train_inds_d1_t1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g_d0_t0_tune_res = _dml_tune( + y, + x, + train_inds_d0_t0, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + g_d0_t1_tune_res = _dml_tune( + y, + x, + train_inds_d0_t1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + g_d1_t0_tune_res = _dml_tune( + y, + x, + train_inds_d1_t0, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + g_d1_t1_tune_res = _dml_tune( + y, + x, + train_inds_d1_t1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) m_tune_res = list() - if self.score == 'observational': - m_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + if self.score == "observational": + m_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g_d0_t0_best_params = [xx.best_params_ for xx in g_d0_t0_tune_res] g_d0_t1_best_params = [xx.best_params_ for xx in g_d0_t1_tune_res] g_d1_t0_best_params = [xx.best_params_ for xx in g_d1_t0_tune_res] g_d1_t1_best_params = [xx.best_params_ for xx in g_d1_t1_tune_res] - if self.score == 'observational': + if self.score == "observational": m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g_d0_t0': g_d0_t0_best_params, - 'ml_g_d0_t1': g_d0_t1_best_params, - 'ml_g_d1_t0': g_d1_t0_best_params, - 'ml_g_d1_t1': g_d1_t1_best_params, - 'ml_m': m_best_params} - tune_res = {'g_d0_t0_tune': g_d0_t0_tune_res, - 'g_d0_t1_tune': g_d0_t1_tune_res, - 'g_d1_t0_tune': g_d1_t0_tune_res, - 'g_d1_t1_tune': g_d1_t1_tune_res, - 'm_tune': m_tune_res} + params = { + "ml_g_d0_t0": g_d0_t0_best_params, + "ml_g_d0_t1": g_d0_t1_best_params, + "ml_g_d1_t0": g_d1_t0_best_params, + "ml_g_d1_t1": g_d1_t1_best_params, + "ml_m": m_best_params, + } + tune_res = { + "g_d0_t0_tune": g_d0_t0_tune_res, + "g_d0_t1_tune": g_d0_t1_tune_res, + "g_d1_t0_tune": g_d1_t0_tune_res, + "g_d1_t1_tune": g_d1_t1_tune_res, + "m_tune": m_tune_res, + } else: - params = {'ml_g_d0_t0': g_d0_t0_best_params, - 'ml_g_d0_t1': g_d0_t1_best_params, - 'ml_g_d1_t0': g_d1_t0_best_params, - 'ml_g_d1_t1': g_d1_t1_best_params} - tune_res = {'g_d0_t0_tune': g_d0_t0_tune_res, - 'g_d0_t1_tune': g_d0_t1_tune_res, - 'g_d1_t0_tune': g_d1_t0_tune_res, - 'g_d1_t1_tune': g_d1_t1_tune_res} - - res = {'params': params, - 'tune_res': tune_res} + params = { + "ml_g_d0_t0": g_d0_t0_best_params, + "ml_g_d0_t1": g_d0_t1_best_params, + "ml_g_d1_t0": g_d1_t0_best_params, + "ml_g_d1_t1": g_d1_t1_best_params, + } + tune_res = { + "g_d0_t0_tune": g_d0_t0_tune_res, + "g_d0_t1_tune": g_d0_t1_tune_res, + "g_d1_t0_tune": g_d1_t0_tune_res, + "g_d1_t1_tune": g_d1_t1_tune_res, + } + + res = {"params": params, "tune_res": tune_res} return res diff --git a/doubleml/did/tests/_utils_did_cs_manual.py b/doubleml/did/tests/_utils_did_cs_manual.py index f7d975e35..0c1ac24b9 100644 --- a/doubleml/did/tests/_utils_did_cs_manual.py +++ b/doubleml/did/tests/_utils_did_cs_manual.py @@ -5,11 +5,24 @@ from ._utils_did_manual import did_dml2 -def fit_did_cs(y, x, d, t, - learner_g, learner_m, all_smpls, score, in_sample_normalization, - n_rep=1, g_d0_t0_params=None, g_d0_t1_params=None, - g_d1_t0_params=None, g_d1_t1_params=None, m_params=None, - trimming_threshold=1e-2): +def fit_did_cs( + y, + x, + d, + t, + learner_g, + learner_m, + all_smpls, + score, + in_sample_normalization, + n_rep=1, + g_d0_t0_params=None, + g_d0_t1_params=None, + g_d1_t0_params=None, + g_d1_t1_params=None, + m_params=None, + trimming_threshold=1e-2, +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -26,14 +39,24 @@ def fit_did_cs(y, x, d, t, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat_d0_t0_list, g_hat_d0_t1_list, g_hat_d1_t0_list, g_hat_d1_t1_list, m_hat_list, \ - p_hat_list, lambda_hat_list = fit_nuisance_did_cs(y, x, d, t, - learner_g, learner_m, - smpls, score, - g_d0_t0_params=g_d0_t0_params, g_d0_t1_params=g_d0_t1_params, - g_d1_t0_params=g_d1_t0_params, g_d1_t1_params=g_d1_t1_params, - m_params=m_params, - trimming_threshold=trimming_threshold) + g_hat_d0_t0_list, g_hat_d0_t1_list, g_hat_d1_t0_list, g_hat_d1_t1_list, m_hat_list, p_hat_list, lambda_hat_list = ( + fit_nuisance_did_cs( + y, + x, + d, + t, + learner_g, + learner_m, + smpls, + score, + g_d0_t0_params=g_d0_t0_params, + g_d0_t1_params=g_d0_t1_params, + g_d1_t0_params=g_d1_t0_params, + g_d1_t1_params=g_d1_t1_params, + m_params=m_params, + trimming_threshold=trimming_threshold, + ) + ) all_g_hat_d0_t0.append(g_hat_d0_t0_list) all_g_hat_d0_t1.append(g_hat_d0_t1_list) @@ -43,16 +66,47 @@ def fit_did_cs(y, x, d, t, all_p_hat.append(p_hat_list) all_lambda_hat.append(lambda_hat_list) - resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, \ - g_hat_d0_t0, g_hat_d0_t1, g_hat_d1_t0, g_hat_d1_t1, \ - m_hat, p_hat, lambda_hat = compute_did_cs_residuals(y, g_hat_d0_t0_list, g_hat_d0_t1_list, - g_hat_d1_t0_list, g_hat_d1_t1_list, - m_hat_list, p_hat_list, - lambda_hat_list, smpls) - - psi_a, psi_b = did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, - g_hat_d0_t0, g_hat_d0_t1, g_hat_d1_t0, g_hat_d1_t1, - m_hat, p_hat, lambda_hat, d, t, score, in_sample_normalization) + ( + resid_d0_t0, + resid_d0_t1, + resid_d1_t0, + resid_d1_t1, + g_hat_d0_t0, + g_hat_d0_t1, + g_hat_d1_t0, + g_hat_d1_t1, + m_hat, + p_hat, + lambda_hat, + ) = compute_did_cs_residuals( + y, + g_hat_d0_t0_list, + g_hat_d0_t1_list, + g_hat_d1_t0_list, + g_hat_d1_t1_list, + m_hat_list, + p_hat_list, + lambda_hat_list, + smpls, + ) + + psi_a, psi_b = did_cs_score_elements( + resid_d0_t0, + resid_d0_t1, + resid_d1_t0, + resid_d1_t1, + g_hat_d0_t0, + g_hat_d0_t1, + g_hat_d1_t0, + g_hat_d1_t1, + m_hat, + p_hat, + lambda_hat, + d, + t, + score, + in_sample_normalization, + ) all_psi_a.append(psi_a) all_psi_b.append(psi_b) @@ -62,76 +116,88 @@ def fit_did_cs(y, x, d, t, theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_g_hat_d0_t0': all_g_hat_d0_t0, 'all_g_hat_d0_t1': all_g_hat_d0_t1, - 'all_g_hat_d1_t0': all_g_hat_d1_t0, 'all_g_hat_d1_t1': all_g_hat_d1_t1, - 'all_m_hat': all_m_hat, - 'all_p_hat': all_p_hat, 'all_lambda_hat': all_lambda_hat, - 'all_psi_a': all_psi_a, 'all_psi_b': all_psi_b} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_g_hat_d0_t0": all_g_hat_d0_t0, + "all_g_hat_d0_t1": all_g_hat_d0_t1, + "all_g_hat_d1_t0": all_g_hat_d1_t0, + "all_g_hat_d1_t1": all_g_hat_d1_t1, + "all_m_hat": all_m_hat, + "all_p_hat": all_p_hat, + "all_lambda_hat": all_lambda_hat, + "all_psi_a": all_psi_a, + "all_psi_b": all_psi_b, + } return res -def fit_nuisance_did_cs(y, x, d, t, - learner_g, learner_m, smpls, score, - g_d0_t0_params=None, g_d0_t1_params=None, - g_d1_t0_params=None, g_d1_t1_params=None, - m_params=None, - trimming_threshold=1e-12): +def fit_nuisance_did_cs( + y, + x, + d, + t, + learner_g, + learner_m, + smpls, + score, + g_d0_t0_params=None, + g_d0_t1_params=None, + g_d1_t0_params=None, + g_d1_t1_params=None, + m_params=None, + trimming_threshold=1e-12, +): ml_g_d0_t0 = clone(learner_g) ml_g_d0_t1 = clone(learner_g) ml_g_d1_t0 = clone(learner_g) ml_g_d1_t1 = clone(learner_g) train_cond_d0_t0 = np.intersect1d(np.where(d == 0)[0], np.where(t == 0)[0]) - g_hat_d0_t0_list = fit_predict(y, x, ml_g_d0_t0, g_d0_t0_params, smpls, - train_cond=train_cond_d0_t0) + g_hat_d0_t0_list = fit_predict(y, x, ml_g_d0_t0, g_d0_t0_params, smpls, train_cond=train_cond_d0_t0) train_cond_d0_t1 = np.intersect1d(np.where(d == 0)[0], np.where(t == 1)[0]) - g_hat_d0_t1_list = fit_predict(y, x, ml_g_d0_t1, g_d0_t1_params, smpls, - train_cond=train_cond_d0_t1) + g_hat_d0_t1_list = fit_predict(y, x, ml_g_d0_t1, g_d0_t1_params, smpls, train_cond=train_cond_d0_t1) train_cond_d1_t0 = np.intersect1d(np.where(d == 1)[0], np.where(t == 0)[0]) - g_hat_d1_t0_list = fit_predict(y, x, ml_g_d1_t0, g_d1_t0_params, smpls, - train_cond=train_cond_d1_t0) + g_hat_d1_t0_list = fit_predict(y, x, ml_g_d1_t0, g_d1_t0_params, smpls, train_cond=train_cond_d1_t0) train_cond_d1_t1 = np.intersect1d(np.where(d == 1)[0], np.where(t == 1)[0]) - g_hat_d1_t1_list = fit_predict(y, x, ml_g_d1_t1, g_d1_t1_params, smpls, - train_cond=train_cond_d1_t1) - if score == 'observational': + g_hat_d1_t1_list = fit_predict(y, x, ml_g_d1_t1, g_d1_t1_params, smpls, train_cond=train_cond_d1_t1) + if score == "observational": ml_m = clone(learner_m) - m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls, - trimming_threshold=trimming_threshold) + m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls, trimming_threshold=trimming_threshold) else: - assert score == 'experimental' + assert score == "experimental" m_hat_list = list() for idx, _ in enumerate(smpls): # fill it up, but its not further used - m_hat_list.append(np.zeros_like(g_hat_d1_t1_list[idx], dtype='float64')) + m_hat_list.append(np.zeros_like(g_hat_d1_t1_list[idx], dtype="float64")) p_hat_list = [] - for (train_index, _) in smpls: + for train_index, _ in smpls: p_hat_list.append(np.mean(d[train_index])) lambda_hat_list = [] - for (train_index, _) in smpls: + for train_index, _ in smpls: lambda_hat_list.append(np.mean(t[train_index])) - return g_hat_d0_t0_list, g_hat_d0_t1_list, g_hat_d1_t0_list, g_hat_d1_t1_list, \ - m_hat_list, p_hat_list, lambda_hat_list + return g_hat_d0_t0_list, g_hat_d0_t1_list, g_hat_d1_t0_list, g_hat_d1_t1_list, m_hat_list, p_hat_list, lambda_hat_list -def compute_did_cs_residuals(y, g_hat_d0_t0_list, g_hat_d0_t1_list, - g_hat_d1_t0_list, g_hat_d1_t1_list, - m_hat_list, p_hat_list, lambda_hat_list, smpls): - g_hat_d0_t0 = np.full_like(y, np.nan, dtype='float64') - g_hat_d0_t1 = np.full_like(y, np.nan, dtype='float64') - g_hat_d1_t0 = np.full_like(y, np.nan, dtype='float64') - g_hat_d1_t1 = np.full_like(y, np.nan, dtype='float64') - m_hat = np.full_like(y, np.nan, dtype='float64') - p_hat = np.full_like(y, np.nan, dtype='float64') - lambda_hat = np.full_like(y, np.nan, dtype='float64') +def compute_did_cs_residuals( + y, g_hat_d0_t0_list, g_hat_d0_t1_list, g_hat_d1_t0_list, g_hat_d1_t1_list, m_hat_list, p_hat_list, lambda_hat_list, smpls +): + g_hat_d0_t0 = np.full_like(y, np.nan, dtype="float64") + g_hat_d0_t1 = np.full_like(y, np.nan, dtype="float64") + g_hat_d1_t0 = np.full_like(y, np.nan, dtype="float64") + g_hat_d1_t1 = np.full_like(y, np.nan, dtype="float64") + m_hat = np.full_like(y, np.nan, dtype="float64") + p_hat = np.full_like(y, np.nan, dtype="float64") + lambda_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): g_hat_d0_t0[test_index] = g_hat_d0_t0_list[idx] g_hat_d0_t1[test_index] = g_hat_d0_t1_list[idx] @@ -145,16 +211,40 @@ def compute_did_cs_residuals(y, g_hat_d0_t0_list, g_hat_d0_t1_list, resid_d0_t1 = y - g_hat_d0_t1 resid_d1_t0 = y - g_hat_d1_t0 resid_d1_t1 = y - g_hat_d1_t1 - return resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, \ - g_hat_d0_t0, g_hat_d0_t1, g_hat_d1_t0, g_hat_d1_t1, \ - m_hat, p_hat, lambda_hat - - -def did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, - g_hat_d0_t0, g_hat_d0_t1, g_hat_d1_t0, g_hat_d1_t1, - m_hat, p_hat, lambda_hat, d, t, score, in_sample_normalization): - - if score == 'observational': + return ( + resid_d0_t0, + resid_d0_t1, + resid_d1_t0, + resid_d1_t1, + g_hat_d0_t0, + g_hat_d0_t1, + g_hat_d1_t0, + g_hat_d1_t1, + m_hat, + p_hat, + lambda_hat, + ) + + +def did_cs_score_elements( + resid_d0_t0, + resid_d0_t1, + resid_d1_t0, + resid_d1_t1, + g_hat_d0_t0, + g_hat_d0_t1, + g_hat_d1_t0, + g_hat_d1_t1, + m_hat, + p_hat, + lambda_hat, + d, + t, + score, + in_sample_normalization, +): + + if score == "observational": if in_sample_normalization: weight_psi_a = np.divide(d, np.mean(d)) weight_g_d1_t1 = weight_psi_a @@ -162,16 +252,14 @@ def did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, weight_g_d0_t1 = -1.0 * weight_psi_a weight_g_d0_t0 = weight_psi_a - weight_resid_d1_t1 = np.divide(np.multiply(d, t), - np.mean(np.multiply(d, t))) - weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0-t), - np.mean(np.multiply(d, 1.0-t))) + weight_resid_d1_t1 = np.divide(np.multiply(d, t), np.mean(np.multiply(d, t))) + weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0 - t), np.mean(np.multiply(d, 1.0 - t))) - prop_weighting = np.divide(m_hat, 1.0-m_hat) - unscaled_d0_t1 = np.multiply(np.multiply(1.0-d, t), prop_weighting) + prop_weighting = np.divide(m_hat, 1.0 - m_hat) + unscaled_d0_t1 = np.multiply(np.multiply(1.0 - d, t), prop_weighting) weight_resid_d0_t1 = -1.0 * np.divide(unscaled_d0_t1, np.mean(unscaled_d0_t1)) - unscaled_d0_t0 = np.multiply(np.multiply(1.0-d, 1.0-t), prop_weighting) + unscaled_d0_t0 = np.multiply(np.multiply(1.0 - d, 1.0 - t), prop_weighting) weight_resid_d0_t0 = np.divide(unscaled_d0_t0, np.mean(unscaled_d0_t0)) else: weight_psi_a = np.divide(d, p_hat) @@ -180,21 +268,19 @@ def did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, weight_g_d0_t1 = -1.0 * weight_psi_a weight_g_d0_t0 = weight_psi_a - weight_resid_d1_t1 = np.divide(np.multiply(d, t), - np.multiply(p_hat, lambda_hat)) - weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0-t), - np.multiply(p_hat, 1.0-lambda_hat)) + weight_resid_d1_t1 = np.divide(np.multiply(d, t), np.multiply(p_hat, lambda_hat)) + weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0 - t), np.multiply(p_hat, 1.0 - lambda_hat)) - prop_weighting = np.divide(m_hat, 1.0-m_hat) - weight_resid_d0_t1 = -1.0 * np.multiply(np.divide(np.multiply(1.0-d, t), - np.multiply(p_hat, lambda_hat)), - prop_weighting) - weight_resid_d0_t0 = np.multiply(np.divide(np.multiply(1.0-d, 1.0-t), - np.multiply(p_hat, 1.0-lambda_hat)), - prop_weighting) + prop_weighting = np.divide(m_hat, 1.0 - m_hat) + weight_resid_d0_t1 = -1.0 * np.multiply( + np.divide(np.multiply(1.0 - d, t), np.multiply(p_hat, lambda_hat)), prop_weighting + ) + weight_resid_d0_t0 = np.multiply( + np.divide(np.multiply(1.0 - d, 1.0 - t), np.multiply(p_hat, 1.0 - lambda_hat)), prop_weighting + ) else: - assert score == 'experimental' + assert score == "experimental" if in_sample_normalization: weight_psi_a = np.ones_like(d) weight_g_d1_t1 = weight_psi_a @@ -202,14 +288,10 @@ def did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, weight_g_d0_t1 = -1.0 * weight_psi_a weight_g_d0_t0 = weight_psi_a - weight_resid_d1_t1 = np.divide(np.multiply(d, t), - np.mean(np.multiply(d, t))) - weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0-t), - np.mean(np.multiply(d, 1.0-t))) - weight_resid_d0_t1 = -1.0 * np.divide(np.multiply(1.0-d, t), - np.mean(np.multiply(1.0-d, t))) - weight_resid_d0_t0 = np.divide(np.multiply(1.0-d, 1.0-t), - np.mean(np.multiply(1.0-d, 1.0-t))) + weight_resid_d1_t1 = np.divide(np.multiply(d, t), np.mean(np.multiply(d, t))) + weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0 - t), np.mean(np.multiply(d, 1.0 - t))) + weight_resid_d0_t1 = -1.0 * np.divide(np.multiply(1.0 - d, t), np.mean(np.multiply(1.0 - d, t))) + weight_resid_d0_t0 = np.divide(np.multiply(1.0 - d, 1.0 - t), np.mean(np.multiply(1.0 - d, 1.0 - t))) else: weight_psi_a = np.ones_like(d) weight_g_d1_t1 = weight_psi_a @@ -217,23 +299,23 @@ def did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, weight_g_d0_t1 = -1.0 * weight_psi_a weight_g_d0_t0 = weight_psi_a - weight_resid_d1_t1 = np.divide(np.multiply(d, t), - np.multiply(p_hat, lambda_hat)) - weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0-t), - np.multiply(p_hat, 1.0-lambda_hat)) - weight_resid_d0_t1 = -1.0 * np.divide(np.multiply(1.0-d, t), - np.multiply(1.0-p_hat, lambda_hat)) - weight_resid_d0_t0 = np.divide(np.multiply(1.0-d, 1.0-t), - np.multiply(1.0-p_hat, 1.0-lambda_hat)) - - psi_b_1 = np.multiply(weight_g_d1_t1, g_hat_d1_t1) + \ - np.multiply(weight_g_d1_t0, g_hat_d1_t0) + \ - np.multiply(weight_g_d0_t0, g_hat_d0_t0) + \ - np.multiply(weight_g_d0_t1, g_hat_d0_t1) - psi_b_2 = np.multiply(weight_resid_d1_t1, resid_d1_t1) + \ - np.multiply(weight_resid_d1_t0, resid_d1_t0) + \ - np.multiply(weight_resid_d0_t0, resid_d0_t0) + \ - np.multiply(weight_resid_d0_t1, resid_d0_t1) + weight_resid_d1_t1 = np.divide(np.multiply(d, t), np.multiply(p_hat, lambda_hat)) + weight_resid_d1_t0 = -1.0 * np.divide(np.multiply(d, 1.0 - t), np.multiply(p_hat, 1.0 - lambda_hat)) + weight_resid_d0_t1 = -1.0 * np.divide(np.multiply(1.0 - d, t), np.multiply(1.0 - p_hat, lambda_hat)) + weight_resid_d0_t0 = np.divide(np.multiply(1.0 - d, 1.0 - t), np.multiply(1.0 - p_hat, 1.0 - lambda_hat)) + + psi_b_1 = ( + np.multiply(weight_g_d1_t1, g_hat_d1_t1) + + np.multiply(weight_g_d1_t0, g_hat_d1_t0) + + np.multiply(weight_g_d0_t0, g_hat_d0_t0) + + np.multiply(weight_g_d0_t1, g_hat_d0_t1) + ) + psi_b_2 = ( + np.multiply(weight_resid_d1_t1, resid_d1_t1) + + np.multiply(weight_resid_d1_t0, resid_d1_t0) + + np.multiply(weight_resid_d0_t0, resid_d0_t0) + + np.multiply(weight_resid_d0_t1, resid_d0_t1) + ) psi_a = -1.0 * weight_psi_a psi_b = psi_b_1 + psi_b_2 @@ -241,37 +323,31 @@ def did_cs_score_elements(resid_d0_t0, resid_d0_t1, resid_d1_t0, resid_d1_t1, return psi_a, psi_b -def tune_nuisance_did_cs(y, x, d, t, ml_g, ml_m, smpls, score, n_folds_tune, - param_grid_g, param_grid_m): +def tune_nuisance_did_cs(y, x, d, t, ml_g, ml_m, smpls, score, n_folds_tune, param_grid_g, param_grid_m): smpls_d0_t0 = np.intersect1d(np.where(d == 0)[0], np.where(t == 0)[0]) smpls_d0_t1 = np.intersect1d(np.where(d == 0)[0], np.where(t == 1)[0]) smpls_d1_t0 = np.intersect1d(np.where(d == 1)[0], np.where(t == 0)[0]) smpls_d1_t1 = np.intersect1d(np.where(d == 1)[0], np.where(t == 1)[0]) - g_d0_t0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=smpls_d0_t0) - g_d0_t1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=smpls_d0_t1) - g_d1_t0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=smpls_d1_t0) - g_d1_t1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=smpls_d1_t1) + g_d0_t0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=smpls_d0_t0) + g_d0_t1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=smpls_d0_t1) + g_d1_t0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=smpls_d1_t0) + g_d1_t1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=smpls_d1_t1) g_d0_t0_best_params = [xx.best_params_ for xx in g_d0_t0_tune_res] g_d0_t1_best_params = [xx.best_params_ for xx in g_d0_t1_tune_res] g_d1_t0_best_params = [xx.best_params_ for xx in g_d1_t0_tune_res] g_d1_t1_best_params = [xx.best_params_ for xx in g_d1_t1_tune_res] - if score == 'observational': + if score == "observational": m_tune_res = tune_grid_search(d, x, ml_m, smpls, param_grid_m, n_folds_tune) m_best_params = [xx.best_params_ for xx in m_tune_res] else: - assert score == 'experimental' + assert score == "experimental" m_best_params = None - return g_d0_t0_best_params, g_d0_t1_best_params, \ - g_d1_t0_best_params, g_d1_t1_best_params, m_best_params + return g_d0_t0_best_params, g_d0_t1_best_params, g_d1_t0_best_params, g_d1_t1_best_params, m_best_params def fit_sensitivity_elements_did_cs(y, d, t, all_coef, predictions, score, in_sample_normalization, n_rep): @@ -285,76 +361,93 @@ def fit_sensitivity_elements_did_cs(y, d, t, all_coef, predictions, score, in_sa for i_rep in range(n_rep): - g_hat_d0_t0 = predictions['ml_g_d0_t0'][:, i_rep, 0] - g_hat_d0_t1 = predictions['ml_g_d0_t1'][:, i_rep, 0] - g_hat_d1_t0 = predictions['ml_g_d1_t0'][:, i_rep, 0] - g_hat_d1_t1 = predictions['ml_g_d1_t1'][:, i_rep, 0] + g_hat_d0_t0 = predictions["ml_g_d0_t0"][:, i_rep, 0] + g_hat_d0_t1 = predictions["ml_g_d0_t1"][:, i_rep, 0] + g_hat_d1_t0 = predictions["ml_g_d1_t0"][:, i_rep, 0] + g_hat_d1_t1 = predictions["ml_g_d1_t1"][:, i_rep, 0] - d0t0 = np.multiply(1.0-d, 1.0-t) - d0t1 = np.multiply(1.0-d, t) - d1t0 = np.multiply(d, 1.0-t) + d0t0 = np.multiply(1.0 - d, 1.0 - t) + d0t1 = np.multiply(1.0 - d, t) + d1t0 = np.multiply(d, 1.0 - t) d1t1 = np.multiply(d, t) - g_hat = np.multiply(d0t0, g_hat_d0_t0) + np.multiply(d0t1, g_hat_d0_t1) + \ - np.multiply(d1t0, g_hat_d1_t0) + np.multiply(d1t1, g_hat_d1_t1) + g_hat = ( + np.multiply(d0t0, g_hat_d0_t0) + + np.multiply(d0t1, g_hat_d0_t1) + + np.multiply(d1t0, g_hat_d1_t0) + + np.multiply(d1t1, g_hat_d1_t1) + ) sigma2_score_element = np.square(y - g_hat) sigma2[0, i_rep, 0] = np.mean(sigma2_score_element) psi_sigma2[:, i_rep, 0] = sigma2_score_element - sigma2[0, i_rep, 0] p_hat = np.mean(d) lambda_hat = np.mean(t) - if score == 'observational': - m_hat = predictions['ml_m'][:, i_rep, 0] - propensity_weight_d0 = np.divide(m_hat, 1.0-m_hat) + if score == "observational": + m_hat = predictions["ml_m"][:, i_rep, 0] + propensity_weight_d0 = np.divide(m_hat, 1.0 - m_hat) if in_sample_normalization: weight_d0t1 = np.multiply(d0t1, propensity_weight_d0) weight_d0t0 = np.multiply(d0t0, propensity_weight_d0) - m_alpha_1 = np.divide(1.0, np.mean(d1t1)) + \ - np.divide(1.0, np.mean(d1t0)) + \ - np.divide(propensity_weight_d0, np.mean(weight_d0t1)) + \ - np.divide(propensity_weight_d0, np.mean(weight_d0t0)) + m_alpha_1 = ( + np.divide(1.0, np.mean(d1t1)) + + np.divide(1.0, np.mean(d1t0)) + + np.divide(propensity_weight_d0, np.mean(weight_d0t1)) + + np.divide(propensity_weight_d0, np.mean(weight_d0t0)) + ) m_alpha = np.multiply(np.divide(d, p_hat), m_alpha_1) - rr = np.divide(d1t1, np.mean(d1t1)) - \ - np.divide(d1t0, np.mean(d1t0)) - \ - np.divide(weight_d0t1, np.mean(weight_d0t1)) + \ - np.divide(weight_d0t0, np.mean(weight_d0t0)) + rr = ( + np.divide(d1t1, np.mean(d1t1)) + - np.divide(d1t0, np.mean(d1t0)) + - np.divide(weight_d0t1, np.mean(weight_d0t1)) + + np.divide(weight_d0t0, np.mean(weight_d0t0)) + ) else: - m_alpha_1 = np.divide(1.0, np.multiply(p_hat, lambda_hat)) + \ - np.divide(1.0, np.multiply(p_hat, 1.0-lambda_hat)) + \ - np.divide(propensity_weight_d0, np.multiply(p_hat, lambda_hat)) + \ - np.divide(propensity_weight_d0, np.multiply(p_hat, 1.0-lambda_hat)) + m_alpha_1 = ( + np.divide(1.0, np.multiply(p_hat, lambda_hat)) + + np.divide(1.0, np.multiply(p_hat, 1.0 - lambda_hat)) + + np.divide(propensity_weight_d0, np.multiply(p_hat, lambda_hat)) + + np.divide(propensity_weight_d0, np.multiply(p_hat, 1.0 - lambda_hat)) + ) m_alpha = np.multiply(np.divide(d, p_hat), m_alpha_1) - rr = np.divide(d1t1, np.multiply(p_hat, lambda_hat)) - \ - np.divide(d1t0, np.multiply(p_hat, 1.0-lambda_hat)) - \ - np.multiply(np.divide(d0t1, np.multiply(p_hat, lambda_hat)), propensity_weight_d0) + \ - np.multiply(np.divide(d0t0, np.multiply(p_hat, 1.0-lambda_hat)), propensity_weight_d0) + rr = ( + np.divide(d1t1, np.multiply(p_hat, lambda_hat)) + - np.divide(d1t0, np.multiply(p_hat, 1.0 - lambda_hat)) + - np.multiply(np.divide(d0t1, np.multiply(p_hat, lambda_hat)), propensity_weight_d0) + + np.multiply(np.divide(d0t0, np.multiply(p_hat, 1.0 - lambda_hat)), propensity_weight_d0) + ) else: - assert score == 'experimental' + assert score == "experimental" if in_sample_normalization: - m_alpha = np.divide(1.0, np.mean(d1t1)) + \ - np.divide(1.0, np.mean(d1t0)) + \ - np.divide(1.0, np.mean(d0t1)) + \ - np.divide(1.0, np.mean(d0t0)) - rr = np.divide(d1t1, np.mean(d1t1)) - \ - np.divide(d1t0, np.mean(d1t0)) - \ - np.divide(d0t1, np.mean(d0t1)) + \ - np.divide(d0t0, np.mean(d0t0)) + m_alpha = ( + np.divide(1.0, np.mean(d1t1)) + + np.divide(1.0, np.mean(d1t0)) + + np.divide(1.0, np.mean(d0t1)) + + np.divide(1.0, np.mean(d0t0)) + ) + rr = ( + np.divide(d1t1, np.mean(d1t1)) + - np.divide(d1t0, np.mean(d1t0)) + - np.divide(d0t1, np.mean(d0t1)) + + np.divide(d0t0, np.mean(d0t0)) + ) else: - m_alpha = np.divide(1.0, np.multiply(p_hat, lambda_hat)) + \ - np.divide(1.0, np.multiply(p_hat, 1.0-lambda_hat)) + \ - np.divide(1.0, np.multiply(1.0-p_hat, lambda_hat)) + \ - np.divide(1.0, np.multiply(1.0-p_hat, 1.0-lambda_hat)) - rr = np.divide(d1t1, np.multiply(p_hat, lambda_hat)) - \ - np.divide(d1t0, np.multiply(p_hat, 1.0-lambda_hat)) - \ - np.divide(d0t1, np.multiply(1.0-p_hat, lambda_hat)) + \ - np.divide(d0t0, np.multiply(1.0-p_hat, 1.0-lambda_hat)) + m_alpha = ( + np.divide(1.0, np.multiply(p_hat, lambda_hat)) + + np.divide(1.0, np.multiply(p_hat, 1.0 - lambda_hat)) + + np.divide(1.0, np.multiply(1.0 - p_hat, lambda_hat)) + + np.divide(1.0, np.multiply(1.0 - p_hat, 1.0 - lambda_hat)) + ) + rr = ( + np.divide(d1t1, np.multiply(p_hat, lambda_hat)) + - np.divide(d1t0, np.multiply(p_hat, 1.0 - lambda_hat)) + - np.divide(d0t1, np.multiply(1.0 - p_hat, lambda_hat)) + + np.divide(d0t0, np.multiply(1.0 - p_hat, 1.0 - lambda_hat)) + ) nu2_score_element = np.multiply(2.0, m_alpha) - np.square(rr) nu2[0, i_rep, 0] = np.mean(nu2_score_element) psi_nu2[:, i_rep, 0] = nu2_score_element - nu2[0, i_rep, 0] - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2} + element_dict = {"sigma2": sigma2, "nu2": nu2, "psi_sigma2": psi_sigma2, "psi_nu2": psi_nu2} return element_dict diff --git a/doubleml/did/tests/_utils_did_manual.py b/doubleml/did/tests/_utils_did_manual.py index cb833b25b..067795bb6 100644 --- a/doubleml/did/tests/_utils_did_manual.py +++ b/doubleml/did/tests/_utils_did_manual.py @@ -5,10 +5,21 @@ from ...tests._utils_boot import boot_manual, draw_weights -def fit_did(y, x, d, - learner_g, learner_m, all_smpls, score, in_sample_normalization, - n_rep=1, g0_params=None, g1_params=None, m_params=None, - trimming_threshold=1e-2): +def fit_did( + y, + x, + d, + learner_g, + learner_m, + all_smpls, + score, + in_sample_normalization, + n_rep=1, + g0_params=None, + g1_params=None, + m_params=None, + trimming_threshold=1e-2, +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -22,12 +33,19 @@ def fit_did(y, x, d, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat0_list, g_hat1_list, m_hat_list, \ - p_hat_list = fit_nuisance_did(y, x, d, - learner_g, learner_m, smpls, - score, - g0_params=g0_params, g1_params=g1_params, m_params=m_params, - trimming_threshold=trimming_threshold) + g_hat0_list, g_hat1_list, m_hat_list, p_hat_list = fit_nuisance_did( + y, + x, + d, + learner_g, + learner_m, + smpls, + score, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + trimming_threshold=trimming_threshold, + ) all_g_hat0.append(g_hat0_list) all_g_hat1.append(g_hat1_list) @@ -35,10 +53,10 @@ def fit_did(y, x, d, all_p_hat.append(p_hat_list) resid_d0, g_hat0, g_hat1, m_hat, p_hat = compute_did_residuals( - y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls) + y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls + ) - psi_a, psi_b = did_score_elements(g_hat0, g_hat1, m_hat, p_hat, - resid_d0, d, score, in_sample_normalization) + psi_a, psi_b = did_score_elements(g_hat0, g_hat1, m_hat, p_hat, resid_d0, d, score, in_sample_normalization) all_psi_a.append(psi_a) all_psi_b.append(psi_b) @@ -48,51 +66,56 @@ def fit_did(y, x, d, theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_g_hat0': all_g_hat0, 'all_g_hat1': all_g_hat1, 'all_m_hat': all_m_hat, 'all_p_hat': all_p_hat, - 'all_psi_a': all_psi_a, 'all_psi_b': all_psi_b} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_g_hat0": all_g_hat0, + "all_g_hat1": all_g_hat1, + "all_m_hat": all_m_hat, + "all_p_hat": all_p_hat, + "all_psi_a": all_psi_a, + "all_psi_b": all_psi_b, + } return res -def fit_nuisance_did(y, x, d, learner_g, learner_m, smpls, score, - g0_params=None, g1_params=None, m_params=None, - trimming_threshold=1e-12): +def fit_nuisance_did( + y, x, d, learner_g, learner_m, smpls, score, g0_params=None, g1_params=None, m_params=None, trimming_threshold=1e-12 +): ml_g0 = clone(learner_g) ml_g1 = clone(learner_g) train_cond0 = np.where(d == 0)[0] - g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) train_cond1 = np.where(d == 1)[0] - g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) - if score == 'experimental': + g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) + if score == "experimental": m_hat_list = list() for idx, _ in enumerate(smpls): # fill it up, but its not further used - m_hat_list.append(np.zeros_like(g_hat0_list[idx], dtype='float64')) + m_hat_list.append(np.zeros_like(g_hat0_list[idx], dtype="float64")) else: - assert score == 'observational' + assert score == "observational" ml_m = clone(learner_m) - m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls, - trimming_threshold=trimming_threshold) + m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls, trimming_threshold=trimming_threshold) p_hat_list = [] - for (train_index, _) in smpls: + for train_index, _ in smpls: p_hat_list.append(np.mean(d[train_index])) return g_hat0_list, g_hat1_list, m_hat_list, p_hat_list def compute_did_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls): - resid_d0 = np.full_like(y, np.nan, dtype='float64') - g_hat0 = np.full_like(y, np.nan, dtype='float64') - g_hat1 = np.full_like(y, np.nan, dtype='float64') - m_hat = np.full_like(y, np.nan, dtype='float64') - p_hat = np.full_like(y, np.nan, dtype='float64') + resid_d0 = np.full_like(y, np.nan, dtype="float64") + g_hat0 = np.full_like(y, np.nan, dtype="float64") + g_hat1 = np.full_like(y, np.nan, dtype="float64") + m_hat = np.full_like(y, np.nan, dtype="float64") + p_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): resid_d0[test_index] = y[test_index] - g_hat0_list[idx] g_hat0[test_index] = g_hat0_list[idx] @@ -105,7 +128,7 @@ def compute_did_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, s def did_dml2(psi_a, psi_b): n_obs = len(psi_a) - theta_hat = - np.mean(psi_b) / np.mean(psi_a) + theta_hat = -np.mean(psi_b) / np.mean(psi_a) se = np.sqrt(var_did(theta_hat, psi_a, psi_b, n_obs)) return theta_hat, se @@ -113,46 +136,45 @@ def did_dml2(psi_a, psi_b): def did_score_elements(g_hat0, g_hat1, m_hat, p_hat, resid_d0, d, score, in_sample_normalization): - if score == 'observational': + if score == "observational": if in_sample_normalization: weight_psi_a = np.divide(d, np.mean(d)) - propensity_weight = np.multiply(1.0-d, np.divide(m_hat, 1.0-m_hat)) + propensity_weight = np.multiply(1.0 - d, np.divide(m_hat, 1.0 - m_hat)) weight_resid_d0 = np.divide(d, np.mean(d)) - np.divide(propensity_weight, np.mean(propensity_weight)) else: weight_psi_a = np.divide(d, p_hat) - weight_resid_d0 = np.divide(d-m_hat, np.multiply(p_hat, 1.0-m_hat)) + weight_resid_d0 = np.divide(d - m_hat, np.multiply(p_hat, 1.0 - m_hat)) psi_b_1 = np.zeros_like(d) else: - assert score == 'experimental' + assert score == "experimental" if in_sample_normalization: weight_psi_a = np.ones_like(d) weight_g0 = np.divide(d, np.mean(d)) - 1.0 weight_g1 = 1.0 - np.divide(d, np.mean(d)) - weight_resid_d0 = np.divide(d, np.mean(d)) - np.divide(1.0-d, np.mean(1.0-d)) + weight_resid_d0 = np.divide(d, np.mean(d)) - np.divide(1.0 - d, np.mean(1.0 - d)) else: weight_psi_a = np.ones_like(d) weight_g0 = np.divide(d, p_hat) - 1.0 weight_g1 = 1.0 - np.divide(d, p_hat) - weight_resid_d0 = np.divide(d-p_hat, np.multiply(p_hat, 1.0-p_hat)) + weight_resid_d0 = np.divide(d - p_hat, np.multiply(p_hat, 1.0 - p_hat)) - psi_b_1 = np.multiply(weight_g0, g_hat0) + np.multiply(weight_g1, g_hat1) + psi_b_1 = np.multiply(weight_g0, g_hat0) + np.multiply(weight_g1, g_hat1) psi_a = -1.0 * weight_psi_a - psi_b = psi_b_1 + np.multiply(weight_resid_d0, resid_d0) + psi_b = psi_b_1 + np.multiply(weight_resid_d0, resid_d0) return psi_a, psi_b def var_did(theta, psi_a, psi_b, n_obs): J = np.mean(psi_a) - var = 1/n_obs * np.mean(np.power(np.multiply(psi_a, theta) + psi_b, 2)) / np.power(J, 2) + var = 1 / n_obs * np.mean(np.power(np.multiply(psi_a, theta) + psi_b, 2)) / np.power(J, 2) return var -def boot_did(y, thetas, ses, all_psi_a, all_psi_b, - all_smpls, bootstrap, n_rep_boot, n_rep=1, apply_cross_fitting=True): +def boot_did(y, thetas, ses, all_psi_a, all_psi_b, all_smpls, bootstrap, n_rep_boot, n_rep=1, apply_cross_fitting=True): all_boot_t_stat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] @@ -163,8 +185,8 @@ def boot_did(y, thetas, ses, all_psi_a, all_psi_b, n_obs = len(test_index) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_did_single_split( - thetas[i_rep], all_psi_a[i_rep], all_psi_b[i_rep], smpls, - ses[i_rep], weights, n_rep_boot, apply_cross_fitting) + thetas[i_rep], all_psi_a[i_rep], all_psi_b[i_rep], smpls, ses[i_rep], weights, n_rep_boot, apply_cross_fitting + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -172,8 +194,7 @@ def boot_did(y, thetas, ses, all_psi_a, all_psi_b, return boot_t_stat -def boot_did_single_split(theta, psi_a, psi_b, - smpls, se, weights, n_rep_boot, apply_cross_fitting): +def boot_did_single_split(theta, psi_a, psi_b, smpls, se, weights, n_rep_boot, apply_cross_fitting): if apply_cross_fitting: J = np.mean(psi_a) @@ -187,22 +208,19 @@ def boot_did_single_split(theta, psi_a, psi_b, return boot_t_stat -def tune_nuisance_did(y, x, d, ml_g, ml_m, smpls, score, n_folds_tune, - param_grid_g, param_grid_m): +def tune_nuisance_did(y, x, d, ml_g, ml_m, smpls, score, n_folds_tune, param_grid_g, param_grid_m): train_cond0 = np.where(d == 0)[0] - g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond0) + g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond0) g0_best_params = [xx.best_params_ for xx in g0_tune_res] train_cond1 = np.where(d == 1)[0] - g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond1) + g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond1) g1_best_params = [xx.best_params_ for xx in g1_tune_res] - if score == 'experimental': + if score == "experimental": m_best_params = None else: - assert score == 'observational' + assert score == "observational" m_tune_res = tune_grid_search(d, x, ml_m, smpls, param_grid_m, n_folds_tune) m_best_params = [xx.best_params_ for xx in m_tune_res] @@ -220,38 +238,36 @@ def fit_sensitivity_elements_did(y, d, all_coef, predictions, score, in_sample_n for i_rep in range(n_rep): - g_hat0 = predictions['ml_g0'][:, i_rep, 0] - g_hat1 = predictions['ml_g1'][:, i_rep, 0] + g_hat0 = predictions["ml_g0"][:, i_rep, 0] + g_hat1 = predictions["ml_g1"][:, i_rep, 0] - sigma2_score_element = np.square(y - np.multiply(d, g_hat1) - np.multiply(1.0-d, g_hat0)) + sigma2_score_element = np.square(y - np.multiply(d, g_hat1) - np.multiply(1.0 - d, g_hat0)) sigma2[0, i_rep, 0] = np.mean(sigma2_score_element) psi_sigma2[:, i_rep, 0] = sigma2_score_element - sigma2[0, i_rep, 0] - if score == 'observational': - m_hat = predictions['ml_m'][:, i_rep, 0] - propensity_weight_d0 = np.divide(m_hat, 1.0-m_hat) + if score == "observational": + m_hat = predictions["ml_m"][:, i_rep, 0] + propensity_weight_d0 = np.divide(m_hat, 1.0 - m_hat) if in_sample_normalization: m_alpha_1 = np.divide(d, np.mean(d)) - m_alpha_2 = np.divide(1, np.mean(d)) + \ - np.divide(propensity_weight_d0, np.mean(np.multiply(1.0-d, propensity_weight_d0))) + m_alpha_2 = np.divide(1, np.mean(d)) + np.divide( + propensity_weight_d0, np.mean(np.multiply(1.0 - d, propensity_weight_d0)) + ) m_alpha = np.multiply(m_alpha_1, m_alpha_2) - rr_1 = np.multiply(1.0-d, propensity_weight_d0) + rr_1 = np.multiply(1.0 - d, propensity_weight_d0) rr = np.divide(d, np.mean(d)) - np.divide(rr_1, np.mean(rr_1)) else: m_alpha_1 = np.divide(d, np.square(np.mean(d))) - m_alpha = np.multiply(m_alpha_1, 1.0+propensity_weight_d0) - rr = np.divide(d, np.mean(d)) - np.multiply(np.divide(1.0-d, np.mean(d)), propensity_weight_d0) + m_alpha = np.multiply(m_alpha_1, 1.0 + propensity_weight_d0) + rr = np.divide(d, np.mean(d)) - np.multiply(np.divide(1.0 - d, np.mean(d)), propensity_weight_d0) else: - assert score == 'experimental' - m_alpha = np.divide(1.0, np.mean(d)) + np.divide(1.0, 1.0-np.mean(d)) - rr = np.divide(d, np.mean(d)) - np.divide(1.0-d, 1.0-np.mean(d)) + assert score == "experimental" + m_alpha = np.divide(1.0, np.mean(d)) + np.divide(1.0, 1.0 - np.mean(d)) + rr = np.divide(d, np.mean(d)) - np.divide(1.0 - d, 1.0 - np.mean(d)) nu2_score_element = np.multiply(2.0, m_alpha) - np.square(rr) nu2[0, i_rep, 0] = np.mean(nu2_score_element) psi_nu2[:, i_rep, 0] = nu2_score_element - nu2[0, i_rep, 0] - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2} + element_dict = {"sigma2": sigma2, "nu2": nu2, "psi_sigma2": psi_sigma2, "psi_nu2": psi_nu2} return element_dict diff --git a/doubleml/did/tests/conftest.py b/doubleml/did/tests/conftest.py index b2cf2c99d..90e8394c2 100644 --- a/doubleml/did/tests/conftest.py +++ b/doubleml/did/tests/conftest.py @@ -4,10 +4,7 @@ from doubleml.datasets import make_did_SZ2020 -@pytest.fixture(scope='session', - params=[(500, 1), - (1000, 1), - (1000, 2)]) +@pytest.fixture(scope="session", params=[(500, 1), (1000, 1), (1000, 2)]) def generate_data_did(request): params = request.param np.random.seed(1111) @@ -16,15 +13,12 @@ def generate_data_did(request): dpg = params[1] # generating data - data = make_did_SZ2020(n, dgp_type=dpg, return_type='array') + data = make_did_SZ2020(n, dgp_type=dpg, return_type="array") return data -@pytest.fixture(scope='session', - params=[(500, 1), - (1000, 1), - (1000, 2)]) +@pytest.fixture(scope="session", params=[(500, 1), (1000, 1), (1000, 2)]) def generate_data_did_cs(request): params = request.param np.random.seed(1111) @@ -33,6 +27,6 @@ def generate_data_did_cs(request): dpg = params[1] # generating data - data = make_did_SZ2020(n, dgp_type=dpg, cross_sectional_data=True, return_type='array') + data = make_did_SZ2020(n, dgp_type=dpg, cross_sectional_data=True, return_type="array") return data diff --git a/doubleml/did/tests/test_did.py b/doubleml/did/tests/test_did.py index 872ac9acf..a6160b39e 100644 --- a/doubleml/did/tests/test_did.py +++ b/doubleml/did/tests/test_did.py @@ -12,36 +12,38 @@ from ._utils_did_manual import boot_did, fit_did, fit_sensitivity_elements_did -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['observational', 'experimental']) +@pytest.fixture(scope="module", params=["observational", "experimental"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def in_sample_normalization(request): return request.param -@pytest.fixture(scope='module', - params=[0.1]) +@pytest.fixture(scope="module", params=[0.1]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_did_fixture(generate_data_did, learner, score, in_sample_normalization, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -58,96 +60,116 @@ def dml_did_fixture(generate_data_did, learner, score, in_sample_normalization, obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) np.random.seed(3141) - dml_did_obj = dml.DoubleMLDID(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - in_sample_normalization=in_sample_normalization, - draw_sample_splitting=False, - trimming_threshold=trimming_threshold) + dml_did_obj = dml.DoubleMLDID( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + in_sample_normalization=in_sample_normalization, + draw_sample_splitting=False, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_did_obj.set_sample_splitting(all_smpls=all_smpls) dml_did_obj.fit() np.random.seed(3141) - res_manual = fit_did(y, x, d, - clone(learner[0]), clone(learner[1]), - all_smpls, score, in_sample_normalization, - trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_did_obj.coef, - 'coef_manual': res_manual['theta'], - 'se': dml_did_obj.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_did( + y, + x, + d, + clone(learner[0]), + clone(learner[1]), + all_smpls, + score, + in_sample_normalization, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_did_obj.coef, + "coef_manual": res_manual["theta"], + "se": dml_did_obj.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_did(y, res_manual['thetas'], res_manual['ses'], - res_manual['all_psi_a'], res_manual['all_psi_b'], - all_smpls, bootstrap, n_rep_boot) + boot_t_stat = boot_did( + y, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_psi_a"], + res_manual["all_psi_b"], + all_smpls, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_did_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_did_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_did_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) # sensitivity tests - res_dict['sensitivity_elements'] = dml_did_obj.sensitivity_elements - res_dict['sensitivity_elements_manual'] = fit_sensitivity_elements_did(y, d, - all_coef=dml_did_obj.all_coef, - predictions=dml_did_obj.predictions, - score=score, - in_sample_normalization=in_sample_normalization, - n_rep=1) + res_dict["sensitivity_elements"] = dml_did_obj.sensitivity_elements + res_dict["sensitivity_elements_manual"] = fit_sensitivity_elements_did( + y, + d, + all_coef=dml_did_obj.all_coef, + predictions=dml_did_obj.predictions, + score=score, + in_sample_normalization=in_sample_normalization, + n_rep=1, + ) # check if sensitivity score with rho=0 gives equal asymptotic standard deviation dml_did_obj.sensitivity_analysis(rho=0.0) - res_dict['sensitivity_ses'] = dml_did_obj.sensitivity_params['se'] + res_dict["sensitivity_ses"] = dml_did_obj.sensitivity_params["se"] return res_dict @pytest.mark.ci def test_dml_did_coef(dml_did_fixture): - assert math.isclose(dml_did_fixture['coef'][0], - dml_did_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_fixture["coef"][0], dml_did_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_se(dml_did_fixture): - assert math.isclose(dml_did_fixture['se'][0], - dml_did_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_fixture["se"][0], dml_did_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_boot(dml_did_fixture): - for bootstrap in dml_did_fixture['boot_methods']: - assert np.allclose(dml_did_fixture['boot_t_stat' + bootstrap], - dml_did_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_did_fixture["boot_methods"]: + assert np.allclose( + dml_did_fixture["boot_t_stat" + bootstrap], + dml_did_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_did_sensitivity(dml_did_fixture): - sensitivity_element_names = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2'] + sensitivity_element_names = ["sigma2", "nu2", "psi_sigma2", "psi_nu2"] for sensitivity_element in sensitivity_element_names: - assert np.allclose(dml_did_fixture['sensitivity_elements'][sensitivity_element], - dml_did_fixture['sensitivity_elements_manual'][sensitivity_element], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_did_fixture["sensitivity_elements"][sensitivity_element], + dml_did_fixture["sensitivity_elements_manual"][sensitivity_element], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_did_sensitivity_rho0(dml_did_fixture): - assert np.allclose(dml_did_fixture['se'], - dml_did_fixture['sensitivity_ses']['lower'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_did_fixture['se'], - dml_did_fixture['sensitivity_ses']['upper'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_did_fixture["se"], dml_did_fixture["sensitivity_ses"]["lower"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_did_fixture["se"], dml_did_fixture["sensitivity_ses"]["upper"], rtol=1e-9, atol=1e-4) @pytest.mark.ci @@ -163,25 +185,21 @@ def test_dml_did_experimental(generate_data_did, in_sample_normalization, learne obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) np.random.seed(3141) - dml_did_obj_without_ml_m = dml.DoubleMLDID(obj_dml_data, - ml_g, - score='experimental', - in_sample_normalization=in_sample_normalization) + dml_did_obj_without_ml_m = dml.DoubleMLDID( + obj_dml_data, ml_g, score="experimental", in_sample_normalization=in_sample_normalization + ) dml_did_obj_without_ml_m.fit() np.random.seed(3141) - dml_did_obj_with_ml_m = dml.DoubleMLDID(obj_dml_data, - ml_g, ml_m, - score='experimental', - in_sample_normalization=in_sample_normalization) + dml_did_obj_with_ml_m = dml.DoubleMLDID( + obj_dml_data, ml_g, ml_m, score="experimental", in_sample_normalization=in_sample_normalization + ) dml_did_obj_with_ml_m.fit() - assert math.isclose(dml_did_obj_with_ml_m.coef[0], - dml_did_obj_without_ml_m.coef[0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_obj_with_ml_m.coef[0], dml_did_obj_without_ml_m.coef[0], rel_tol=1e-9, abs_tol=1e-4) - msg = ('A learner ml_m has been provided for score = "experimental" but will be ignored. ' - 'A learner ml_m is not required for estimation.') + msg = ( + 'A learner ml_m has been provided for score = "experimental" but will be ignored. ' + "A learner ml_m is not required for estimation." + ) with pytest.warns(UserWarning, match=msg): - dml.DoubleMLDID(obj_dml_data, ml_g, ml_m, - score='experimental', - in_sample_normalization=in_sample_normalization) + dml.DoubleMLDID(obj_dml_data, ml_g, ml_m, score="experimental", in_sample_normalization=in_sample_normalization) diff --git a/doubleml/did/tests/test_did_cs.py b/doubleml/did/tests/test_did_cs.py index d9f63e9ca..ae6335883 100644 --- a/doubleml/did/tests/test_did_cs.py +++ b/doubleml/did/tests/test_did_cs.py @@ -13,36 +13,38 @@ from ._utils_did_manual import boot_did -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['observational', 'experimental']) +@pytest.fixture(scope="module", params=["observational", "experimental"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def in_sample_normalization(request): return request.param -@pytest.fixture(scope='module', - params=[0.1]) +@pytest.fixture(scope="module", params=[0.1]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_did_cs_fixture(generate_data_did_cs, learner, score, in_sample_normalization, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -56,99 +58,121 @@ def dml_did_cs_fixture(generate_data_did_cs, learner, score, in_sample_normaliza np.random.seed(3141) n_obs = len(y) - all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d+2*t) + all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d + 2 * t) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, t=t) np.random.seed(3141) - dml_did_cs_obj = dml.DoubleMLDIDCS(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - in_sample_normalization=in_sample_normalization, - draw_sample_splitting=False, - trimming_threshold=trimming_threshold) + dml_did_cs_obj = dml.DoubleMLDIDCS( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + in_sample_normalization=in_sample_normalization, + draw_sample_splitting=False, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_did_cs_obj.set_sample_splitting(all_smpls=all_smpls) dml_did_cs_obj.fit() np.random.seed(3141) - res_manual = fit_did_cs(y, x, d, t, - clone(learner[0]), clone(learner[1]), - all_smpls, score, in_sample_normalization, - trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_did_cs_obj.coef, - 'coef_manual': res_manual['theta'], - 'se': dml_did_cs_obj.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_did_cs( + y, + x, + d, + t, + clone(learner[0]), + clone(learner[1]), + all_smpls, + score, + in_sample_normalization, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_did_cs_obj.coef, + "coef_manual": res_manual["theta"], + "se": dml_did_cs_obj.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_did(y, res_manual['thetas'], res_manual['ses'], - res_manual['all_psi_a'], res_manual['all_psi_b'], - all_smpls, bootstrap, n_rep_boot) + boot_t_stat = boot_did( + y, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_psi_a"], + res_manual["all_psi_b"], + all_smpls, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_did_cs_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_did_cs_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_did_cs_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) # sensitivity tests - res_dict['sensitivity_elements'] = dml_did_cs_obj.sensitivity_elements - res_dict['sensitivity_elements_manual'] = fit_sensitivity_elements_did_cs(y, d, t, - all_coef=dml_did_cs_obj.all_coef, - predictions=dml_did_cs_obj.predictions, - score=score, - in_sample_normalization=in_sample_normalization, - n_rep=1) + res_dict["sensitivity_elements"] = dml_did_cs_obj.sensitivity_elements + res_dict["sensitivity_elements_manual"] = fit_sensitivity_elements_did_cs( + y, + d, + t, + all_coef=dml_did_cs_obj.all_coef, + predictions=dml_did_cs_obj.predictions, + score=score, + in_sample_normalization=in_sample_normalization, + n_rep=1, + ) # check if sensitivity score with rho=0 gives equal asymptotic standard deviation dml_did_cs_obj.sensitivity_analysis(rho=0.0) - res_dict['sensitivity_ses'] = dml_did_cs_obj.sensitivity_params['se'] + res_dict["sensitivity_ses"] = dml_did_cs_obj.sensitivity_params["se"] return res_dict @pytest.mark.ci def test_dml_did_cs_coef(dml_did_cs_fixture): - assert math.isclose(dml_did_cs_fixture['coef'][0], - dml_did_cs_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_cs_fixture["coef"][0], dml_did_cs_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_cs_se(dml_did_cs_fixture): - assert math.isclose(dml_did_cs_fixture['se'][0], - dml_did_cs_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_cs_fixture["se"][0], dml_did_cs_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_cs_boot(dml_did_cs_fixture): - for bootstrap in dml_did_cs_fixture['boot_methods']: - assert np.allclose(dml_did_cs_fixture['boot_t_stat' + bootstrap], - dml_did_cs_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_did_cs_fixture["boot_methods"]: + assert np.allclose( + dml_did_cs_fixture["boot_t_stat" + bootstrap], + dml_did_cs_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_did_cs_sensitivity(dml_did_cs_fixture): - sensitivity_element_names = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2'] + sensitivity_element_names = ["sigma2", "nu2", "psi_sigma2", "psi_nu2"] for sensitivity_element in sensitivity_element_names: - assert np.allclose(dml_did_cs_fixture['sensitivity_elements'][sensitivity_element], - dml_did_cs_fixture['sensitivity_elements_manual'][sensitivity_element], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_did_cs_fixture["sensitivity_elements"][sensitivity_element], + dml_did_cs_fixture["sensitivity_elements_manual"][sensitivity_element], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_did_cs_sensitivity_rho0(dml_did_cs_fixture): - assert np.allclose(dml_did_cs_fixture['se'], - dml_did_cs_fixture['sensitivity_ses']['lower'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_did_cs_fixture['se'], - dml_did_cs_fixture['sensitivity_ses']['upper'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_did_cs_fixture["se"], dml_did_cs_fixture["sensitivity_ses"]["lower"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_did_cs_fixture["se"], dml_did_cs_fixture["sensitivity_ses"]["upper"], rtol=1e-9, atol=1e-4) @pytest.mark.ci @@ -164,25 +188,21 @@ def test_dml_did_cs_experimental(generate_data_did_cs, in_sample_normalization, obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, t=t) np.random.seed(3141) - dml_did_obj_without_ml_m = dml.DoubleMLDIDCS(obj_dml_data, - ml_g, - score='experimental', - in_sample_normalization=in_sample_normalization) + dml_did_obj_without_ml_m = dml.DoubleMLDIDCS( + obj_dml_data, ml_g, score="experimental", in_sample_normalization=in_sample_normalization + ) dml_did_obj_without_ml_m.fit() np.random.seed(3141) - dml_did_obj_with_ml_m = dml.DoubleMLDIDCS(obj_dml_data, - ml_g, ml_m, - score='experimental', - in_sample_normalization=in_sample_normalization) + dml_did_obj_with_ml_m = dml.DoubleMLDIDCS( + obj_dml_data, ml_g, ml_m, score="experimental", in_sample_normalization=in_sample_normalization + ) dml_did_obj_with_ml_m.fit() - assert math.isclose(dml_did_obj_with_ml_m.coef[0], - dml_did_obj_without_ml_m.coef[0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_obj_with_ml_m.coef[0], dml_did_obj_without_ml_m.coef[0], rel_tol=1e-9, abs_tol=1e-4) - msg = ('A learner ml_m has been provided for score = "experimental" but will be ignored. ' - 'A learner ml_m is not required for estimation.') + msg = ( + 'A learner ml_m has been provided for score = "experimental" but will be ignored. ' + "A learner ml_m is not required for estimation." + ) with pytest.warns(UserWarning, match=msg): - dml.DoubleMLDIDCS(obj_dml_data, ml_g, ml_m, - score='experimental', - in_sample_normalization=in_sample_normalization) + dml.DoubleMLDIDCS(obj_dml_data, ml_g, ml_m, score="experimental", in_sample_normalization=in_sample_normalization) diff --git a/doubleml/did/tests/test_did_cs_external_predictions.py b/doubleml/did/tests/test_did_cs_external_predictions.py index 20060c4ce..f4a479971 100644 --- a/doubleml/did/tests/test_did_cs_external_predictions.py +++ b/doubleml/did/tests/test_did_cs_external_predictions.py @@ -26,13 +26,7 @@ def doubleml_didcs_fixture(did_score, n_rep): ext_predictions = {"d": {}} dml_data = make_did_SZ2020(n_obs=500, cross_sectional_data=True, return_type="DoubleMLData") all_smpls = draw_smpls(len(dml_data.y), 5, n_rep=n_rep, groups=dml_data.d) - kwargs = { - "obj_dml_data": dml_data, - "score": did_score, - "n_rep": n_rep, - "n_folds": 5, - "draw_sample_splitting": False - } + kwargs = {"obj_dml_data": dml_data, "score": did_score, "n_rep": n_rep, "n_folds": 5, "draw_sample_splitting": False} dml_did_cs = DoubleMLDIDCS(ml_g=LinearRegression(), ml_m=LogisticRegression(), **kwargs) dml_did_cs.set_sample_splitting(all_smpls) np.random.seed(3141) diff --git a/doubleml/did/tests/test_did_cs_tune.py b/doubleml/did/tests/test_did_cs_tune.py index da5edafe6..5ec33e822 100644 --- a/doubleml/did/tests/test_did_cs_tune.py +++ b/doubleml/did/tests/test_did_cs_tune.py @@ -13,52 +13,46 @@ from ._utils_did_manual import boot_did -@pytest.fixture(scope='module', - params=[RandomForestRegressor(random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression()]) +@pytest.fixture(scope="module", params=[LogisticRegression()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=['observational', 'experimental']) +@pytest.fixture(scope="module", params=["observational", "experimental"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def in_sample_normalization(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ in [LogisticRegression] - par_grid = {'C': np.logspace(-4, 2, 10)} + par_grid = {"C": np.logspace(-4, 2, 10)} return par_grid -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_did_cs_fixture(generate_data_did_cs, learner_g, learner_m, score, in_sample_normalization, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -70,24 +64,25 @@ def dml_did_cs_fixture(generate_data_did_cs, learner_g, learner_m, score, in_sam ml_m = clone(learner_m) n_obs = len(y) - all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d+2*t) + all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d + 2 * t) np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, t=t) - dml_did_cs_obj = dml.DoubleMLDIDCS(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - in_sample_normalization=in_sample_normalization, - draw_sample_splitting=False) + dml_did_cs_obj = dml.DoubleMLDIDCS( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + in_sample_normalization=in_sample_normalization, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_did_cs_obj.set_sample_splitting(all_smpls=all_smpls) np.random.seed(3141) # tune hyperparameters - tune_res = dml_did_cs_obj.tune(par_grid, tune_on_folds=tune_on_folds, - n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_did_cs_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLDIDCS) dml_did_cs_obj.fit() @@ -96,71 +91,86 @@ def dml_did_cs_fixture(generate_data_did_cs, learner_g, learner_m, score, in_sam smpls = all_smpls[0] if tune_on_folds: - g_d0_t0_params, g_d0_t1_params, g_d1_t0_params, g_d1_t1_params, \ - m_params = tune_nuisance_did_cs(y, x, d, t, - clone(learner_g), clone(learner_m), - smpls, score, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g_d0_t0_params, g_d0_t1_params, g_d1_t0_params, g_d1_t1_params, m_params = tune_nuisance_did_cs( + y, x, d, t, clone(learner_g), clone(learner_m), smpls, score, n_folds_tune, par_grid["ml_g"], par_grid["ml_m"] + ) else: xx = [(np.arange(len(y)), np.array([]))] - g_d0_t0_params, g_d0_t1_params, g_d1_t0_params, g_d1_t1_params, \ - m_params = tune_nuisance_did_cs(y, x, d, t, - clone(learner_g), clone(learner_m), - xx, score, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g_d0_t0_params, g_d0_t1_params, g_d1_t0_params, g_d1_t1_params, m_params = tune_nuisance_did_cs( + y, x, d, t, clone(learner_g), clone(learner_m), xx, score, n_folds_tune, par_grid["ml_g"], par_grid["ml_m"] + ) g_d0_t0_params = g_d0_t0_params * n_folds g_d0_t1_params = g_d0_t1_params * n_folds g_d1_t0_params = g_d1_t0_params * n_folds g_d1_t1_params = g_d1_t1_params * n_folds - if score == 'observational': + if score == "observational": m_params = m_params * n_folds else: - assert score == 'experimental' + assert score == "experimental" m_params = None - res_manual = fit_did_cs(y, x, d, t, clone(learner_g), clone(learner_m), - all_smpls, score, in_sample_normalization, - g_d0_t0_params=g_d0_t0_params, g_d0_t1_params=g_d0_t1_params, - g_d1_t0_params=g_d1_t0_params, g_d1_t1_params=g_d1_t1_params, - m_params=m_params) - - res_dict = {'coef': dml_did_cs_obj.coef, - 'coef_manual': res_manual['theta'], - 'se': dml_did_cs_obj.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_did_cs( + y, + x, + d, + t, + clone(learner_g), + clone(learner_m), + all_smpls, + score, + in_sample_normalization, + g_d0_t0_params=g_d0_t0_params, + g_d0_t1_params=g_d0_t1_params, + g_d1_t0_params=g_d1_t0_params, + g_d1_t1_params=g_d1_t1_params, + m_params=m_params, + ) + + res_dict = { + "coef": dml_did_cs_obj.coef, + "coef_manual": res_manual["theta"], + "se": dml_did_cs_obj.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_did(y, res_manual['thetas'], res_manual['ses'], - res_manual['all_psi_a'], res_manual['all_psi_b'], - all_smpls, bootstrap, n_rep_boot) + boot_t_stat = boot_did( + y, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_psi_a"], + res_manual["all_psi_b"], + all_smpls, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_did_cs_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_did_cs_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_did_cs_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_did_cs_coef(dml_did_cs_fixture): - assert math.isclose(dml_did_cs_fixture['coef'][0], - dml_did_cs_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_cs_fixture["coef"][0], dml_did_cs_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_cs_se(dml_did_cs_fixture): - assert math.isclose(dml_did_cs_fixture['se'][0], - dml_did_cs_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_cs_fixture["se"][0], dml_did_cs_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_cs_boot(dml_did_cs_fixture): - for bootstrap in dml_did_cs_fixture['boot_methods']: - assert np.allclose(dml_did_cs_fixture['boot_t_stat' + bootstrap], - dml_did_cs_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_did_cs_fixture["boot_methods"]: + assert np.allclose( + dml_did_cs_fixture["boot_t_stat" + bootstrap], + dml_did_cs_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/did/tests/test_did_external_predictions.py b/doubleml/did/tests/test_did_external_predictions.py index 99888546d..9027e7dc7 100644 --- a/doubleml/did/tests/test_did_external_predictions.py +++ b/doubleml/did/tests/test_did_external_predictions.py @@ -26,12 +26,7 @@ def doubleml_did_fixture(did_score, n_rep): ext_predictions = {"d": {}} dml_data = make_did_SZ2020(n_obs=500, return_type="DoubleMLData") all_smpls = draw_smpls(len(dml_data.y), 5, n_rep=n_rep, groups=dml_data.d) - kwargs = { - "obj_dml_data": dml_data, - "score": did_score, - "n_rep": n_rep, - "draw_sample_splitting": False - } + kwargs = {"obj_dml_data": dml_data, "score": did_score, "n_rep": n_rep, "draw_sample_splitting": False} dml_did = DoubleMLDID(ml_g=LinearRegression(), ml_m=LogisticRegression(), **kwargs) dml_did.set_sample_splitting(all_smpls) np.random.seed(3141) diff --git a/doubleml/did/tests/test_did_tune.py b/doubleml/did/tests/test_did_tune.py index eb2324c20..c5a381f3c 100644 --- a/doubleml/did/tests/test_did_tune.py +++ b/doubleml/did/tests/test_did_tune.py @@ -12,53 +12,46 @@ from ._utils_did_manual import boot_did, fit_did, tune_nuisance_did -@pytest.fixture(scope='module', - params=[RandomForestRegressor(random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression()]) +@pytest.fixture(scope="module", params=[LogisticRegression()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=['observational', 'experimental']) +@pytest.fixture(scope="module", params=["observational", "experimental"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def in_sample_normalization(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ in [LogisticRegression] - par_grid = {'C': np.logspace(-4, 2, 10)} + par_grid = {"C": np.logspace(-4, 2, 10)} return par_grid -@pytest.fixture(scope='module') -def dml_did_fixture(generate_data_did, learner_g, learner_m, score, in_sample_normalization, - tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} +@pytest.fixture(scope="module") +def dml_did_fixture(generate_data_did, learner_g, learner_m, score, in_sample_normalization, tune_on_folds): + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -73,18 +66,20 @@ def dml_did_fixture(generate_data_did, learner_g, learner_m, score, in_sample_no np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) - dml_did_obj = dml.DoubleMLDID(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - in_sample_normalization=in_sample_normalization, - draw_sample_splitting=False) + dml_did_obj = dml.DoubleMLDID( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + in_sample_normalization=in_sample_normalization, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_did_obj.set_sample_splitting(all_smpls=all_smpls) # tune hyperparameters - tune_res = dml_did_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_did_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLDID) dml_did_obj.fit() @@ -93,66 +88,82 @@ def dml_did_fixture(generate_data_did, learner_g, learner_m, score, in_sample_no smpls = all_smpls[0] if tune_on_folds: - g0_params, g1_params, m_params = tune_nuisance_did(y, x, d, - clone(learner_g), clone(learner_m), smpls, score, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g0_params, g1_params, m_params = tune_nuisance_did( + y, x, d, clone(learner_g), clone(learner_m), smpls, score, n_folds_tune, par_grid["ml_g"], par_grid["ml_m"] + ) else: xx = [(np.arange(len(y)), np.array([]))] - g0_params, g1_params, m_params = tune_nuisance_did(y, x, d, - clone(learner_g), clone(learner_m), xx, score, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g0_params, g1_params, m_params = tune_nuisance_did( + y, x, d, clone(learner_g), clone(learner_m), xx, score, n_folds_tune, par_grid["ml_g"], par_grid["ml_m"] + ) g0_params = g0_params * n_folds - if score == 'experimental': + if score == "experimental": g1_params = g1_params * n_folds m_params = None else: - assert score == 'observational' + assert score == "observational" g1_params = None m_params = m_params * n_folds - res_manual = fit_did(y, x, d, clone(learner_g), clone(learner_m), - all_smpls, score, in_sample_normalization, - g0_params=g0_params, g1_params=g1_params, m_params=m_params) - - res_dict = {'coef': dml_did_obj.coef, - 'coef_manual': res_manual['theta'], - 'se': dml_did_obj.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_did( + y, + x, + d, + clone(learner_g), + clone(learner_m), + all_smpls, + score, + in_sample_normalization, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + ) + + res_dict = { + "coef": dml_did_obj.coef, + "coef_manual": res_manual["theta"], + "se": dml_did_obj.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_did(y, res_manual['thetas'], res_manual['ses'], - res_manual['all_psi_a'], res_manual['all_psi_b'], - all_smpls, bootstrap, n_rep_boot) + boot_t_stat = boot_did( + y, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_psi_a"], + res_manual["all_psi_b"], + all_smpls, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_did_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_did_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_did_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_did_coef(dml_did_fixture): - assert math.isclose(dml_did_fixture['coef'][0], - dml_did_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_fixture["coef"][0], dml_did_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_se(dml_did_fixture): - assert math.isclose(dml_did_fixture['se'][0], - dml_did_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_did_fixture["se"][0], dml_did_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_did_boot(dml_did_fixture): - for bootstrap in dml_did_fixture['boot_methods']: - assert np.allclose(dml_did_fixture['boot_t_stat' + bootstrap], - dml_did_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_did_fixture["boot_methods"]: + assert np.allclose( + dml_did_fixture["boot_t_stat" + bootstrap], + dml_did_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) From f06879ddeaebdcc4d52dcf4bb03188ef5127dde4 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:02:42 +0100 Subject: [PATCH 06/28] run ruff for irm --- doubleml/irm/apo.py | 21 +++++++------ doubleml/irm/apos.py | 16 +++++----- doubleml/irm/cvar.py | 26 ++++++++++++---- doubleml/irm/iivm.py | 12 ++++--- doubleml/irm/irm.py | 22 ++++++++----- doubleml/irm/lpq.py | 19 ++++++------ doubleml/irm/pq.py | 31 +++++++++---------- doubleml/irm/qte.py | 18 +++++------ doubleml/irm/ssm.py | 21 +++++-------- doubleml/irm/tests/_utils_apo_manual.py | 5 ++- doubleml/irm/tests/_utils_apos_manual.py | 3 +- doubleml/irm/tests/_utils_cvar_manual.py | 4 +-- doubleml/irm/tests/_utils_iivm_manual.py | 3 +- doubleml/irm/tests/_utils_irm_manual.py | 5 ++- doubleml/irm/tests/_utils_lpq_manual.py | 6 ++-- doubleml/irm/tests/_utils_pq_manual.py | 6 ++-- doubleml/irm/tests/_utils_qte_manual.py | 3 +- doubleml/irm/tests/conftest.py | 5 ++- doubleml/irm/tests/test_apo.py | 11 +++---- doubleml/irm/tests/test_apo_classifier.py | 9 +++--- doubleml/irm/tests/test_apo_exceptions.py | 11 +++---- .../tests/test_apo_external_predictions.py | 9 +++--- doubleml/irm/tests/test_apo_tune.py | 9 +++--- .../irm/tests/test_apo_weighted_scores.py | 8 ++--- doubleml/irm/tests/test_apos.py | 8 ++--- doubleml/irm/tests/test_apos_classfier.py | 6 ++-- doubleml/irm/tests/test_apos_exceptions.py | 9 +++--- .../tests/test_apos_external_predictions.py | 9 +++--- .../irm/tests/test_apos_weighted_scores.py | 5 ++- doubleml/irm/tests/test_cvar.py | 10 +++--- doubleml/irm/tests/test_cvar_tune.py | 4 +-- doubleml/irm/tests/test_iivm.py | 9 +++--- doubleml/irm/tests/test_iivm_classifier.py | 9 +++--- .../tests/test_iivm_external_predictions.py | 8 +++-- doubleml/irm/tests/test_iivm_subgroups.py | 7 ++--- doubleml/irm/tests/test_iivm_tune.py | 9 +++--- doubleml/irm/tests/test_irm.py | 9 +++--- doubleml/irm/tests/test_irm_classifier.py | 9 +++--- .../tests/test_irm_external_predictions.py | 8 +++-- doubleml/irm/tests/test_irm_tune.py | 9 +++--- .../irm/tests/test_irm_weighted_scores.py | 5 ++- doubleml/irm/tests/test_irm_with_missings.py | 8 ++--- doubleml/irm/tests/test_lpq.py | 10 +++--- .../tests/test_lpq_external_predictions.py | 7 +++-- doubleml/irm/tests/test_lpq_tune.py | 4 +-- doubleml/irm/tests/test_pq.py | 10 +++--- .../irm/tests/test_pq_external_predictions.py | 7 +++-- doubleml/irm/tests/test_pq_tune.py | 4 +-- doubleml/irm/tests/test_qte.py | 16 +++++----- doubleml/irm/tests/test_qte_exceptions.py | 9 +++--- doubleml/irm/tests/test_ssm.py | 5 ++- doubleml/irm/tests/test_ssm_exceptions.py | 9 +++--- doubleml/irm/tests/test_ssm_tune.py | 7 ++--- pyproject.toml | 6 +++- 54 files changed, 258 insertions(+), 260 deletions(-) diff --git a/doubleml/irm/apo.py b/doubleml/irm/apo.py index 91e028d17..e97157286 100644 --- a/doubleml/irm/apo.py +++ b/doubleml/irm/apo.py @@ -1,18 +1,21 @@ -import numpy as np -import pandas as pd import warnings +import numpy as np +import pandas as pd from sklearn.utils import check_X_y from ..double_ml import DoubleML - -from ..utils.blp import DoubleMLBLP from ..double_ml_score_mixins import LinearScoreMixin - -from ..utils._estimation import _dml_cv_predict, _dml_tune, _get_cond_smpls, _cond_targets, _trimm, \ - _normalize_ipw -from ..utils._checks import _check_score, _check_trimming, _check_weights, _check_finite_predictions, \ - _check_is_propensity, _check_binary_predictions +from ..utils._checks import ( + _check_binary_predictions, + _check_finite_predictions, + _check_is_propensity, + _check_score, + _check_trimming, + _check_weights, +) +from ..utils._estimation import _cond_targets, _dml_cv_predict, _dml_tune, _get_cond_smpls, _normalize_ipw, _trimm +from ..utils.blp import DoubleMLBLP class DoubleMLAPO(LinearScoreMixin, DoubleML): diff --git a/doubleml/irm/apos.py b/doubleml/irm/apos.py index 2a6b5ce1a..1800b7f05 100644 --- a/doubleml/irm/apos.py +++ b/doubleml/irm/apos.py @@ -1,21 +1,19 @@ -import numpy as np -import pandas as pd import copy from collections.abc import Iterable -from sklearn.base import clone - +import numpy as np +import pandas as pd from joblib import Parallel, delayed +from sklearn.base import clone from ..double_ml import DoubleML -from ..double_ml_data import DoubleMLData, DoubleMLClusterData -from .apo import DoubleMLAPO +from ..double_ml_data import DoubleMLClusterData, DoubleMLData from ..double_ml_framework import concat - -from ..utils.resampling import DoubleMLResampling +from ..utils._checks import _check_sample_splitting, _check_score, _check_trimming, _check_weights from ..utils._descriptive import generate_summary -from ..utils._checks import _check_score, _check_trimming, _check_weights, _check_sample_splitting from ..utils.gain_statistics import gain_statistics +from ..utils.resampling import DoubleMLResampling +from .apo import DoubleMLAPO class DoubleMLAPOS: diff --git a/doubleml/irm/cvar.py b/doubleml/irm/cvar.py index c92cb9657..7220b3f7b 100644 --- a/doubleml/irm/cvar.py +++ b/doubleml/irm/cvar.py @@ -1,15 +1,29 @@ import numpy as np from sklearn.base import clone -from sklearn.utils import check_X_y from sklearn.model_selection import StratifiedKFold, train_test_split +from sklearn.utils import check_X_y from ..double_ml import DoubleML -from ..double_ml_score_mixins import LinearScoreMixin -from ..utils._estimation import _dml_cv_predict, _trimm, _predict_zero_one_propensity, \ - _normalize_ipw, _dml_tune, _get_bracket_guess, _solve_ipw_score, _cond_targets from ..double_ml_data import DoubleMLData -from ..utils._checks import _check_score, _check_trimming, _check_zero_one_treatment, _check_treatment, \ - _check_contains_iv, _check_quantile +from ..double_ml_score_mixins import LinearScoreMixin +from ..utils._checks import ( + _check_contains_iv, + _check_quantile, + _check_score, + _check_treatment, + _check_trimming, + _check_zero_one_treatment, +) +from ..utils._estimation import ( + _cond_targets, + _dml_cv_predict, + _dml_tune, + _get_bracket_guess, + _normalize_ipw, + _predict_zero_one_propensity, + _solve_ipw_score, + _trimm, +) class DoubleMLCVAR(LinearScoreMixin, DoubleML): diff --git a/doubleml/irm/iivm.py b/doubleml/irm/iivm.py index c2f85dd4d..68817add2 100644 --- a/doubleml/irm/iivm.py +++ b/doubleml/irm/iivm.py @@ -5,10 +5,14 @@ from ..double_ml import DoubleML from ..double_ml_data import DoubleMLData from ..double_ml_score_mixins import LinearScoreMixin - -from ..utils._estimation import _dml_cv_predict, _get_cond_smpls, _dml_tune, _trimm, _normalize_ipw -from ..utils._checks import _check_score, _check_trimming, _check_finite_predictions, _check_is_propensity, \ - _check_binary_predictions +from ..utils._checks import ( + _check_binary_predictions, + _check_finite_predictions, + _check_is_propensity, + _check_score, + _check_trimming, +) +from ..utils._estimation import _dml_cv_predict, _dml_tune, _get_cond_smpls, _normalize_ipw, _trimm class DoubleMLIIVM(LinearScoreMixin, DoubleML): diff --git a/doubleml/irm/irm.py b/doubleml/irm/irm.py index e5acd45d0..dbacac367 100644 --- a/doubleml/irm/irm.py +++ b/doubleml/irm/irm.py @@ -1,19 +1,25 @@ +import warnings + import numpy as np import pandas as pd -import warnings from sklearn.utils import check_X_y from sklearn.utils.multiclass import type_of_target from ..double_ml import DoubleML - -from ..utils.blp import DoubleMLBLP -from ..utils.policytree import DoubleMLPolicyTree from ..double_ml_data import DoubleMLData from ..double_ml_score_mixins import LinearScoreMixin - -from ..utils._estimation import _dml_cv_predict, _get_cond_smpls, _dml_tune, _trimm, _normalize_ipw, _cond_targets -from ..utils._checks import _check_score, _check_trimming, _check_finite_predictions, _check_is_propensity, _check_integer, \ - _check_weights, _check_binary_predictions +from ..utils._checks import ( + _check_binary_predictions, + _check_finite_predictions, + _check_integer, + _check_is_propensity, + _check_score, + _check_trimming, + _check_weights, +) +from ..utils._estimation import _cond_targets, _dml_cv_predict, _dml_tune, _get_cond_smpls, _normalize_ipw, _trimm +from ..utils.blp import DoubleMLBLP +from ..utils.policytree import DoubleMLPolicyTree class DoubleMLIRM(LinearScoreMixin, DoubleML): diff --git a/doubleml/irm/lpq.py b/doubleml/irm/lpq.py index 31a522f38..1285d4eb3 100644 --- a/doubleml/irm/lpq.py +++ b/doubleml/irm/lpq.py @@ -1,25 +1,24 @@ import numpy as np -from sklearn.utils.multiclass import type_of_target from sklearn.base import clone -from sklearn.utils import check_X_y from sklearn.model_selection import StratifiedKFold, train_test_split +from sklearn.utils import check_X_y +from sklearn.utils.multiclass import type_of_target from ..double_ml import DoubleML -from ..double_ml_score_mixins import NonLinearScoreMixin from ..double_ml_data import DoubleMLData - +from ..double_ml_score_mixins import NonLinearScoreMixin +from ..utils._checks import _check_quantile, _check_score, _check_treatment, _check_trimming, _check_zero_one_treatment from ..utils._estimation import ( - _dml_cv_predict, - _trimm, - _predict_zero_one_propensity, _cond_targets, - _get_bracket_guess, _default_kde, - _normalize_ipw, + _dml_cv_predict, _dml_tune, + _get_bracket_guess, + _normalize_ipw, + _predict_zero_one_propensity, _solve_ipw_score, + _trimm, ) -from ..utils._checks import _check_score, _check_trimming, _check_zero_one_treatment, _check_treatment, _check_quantile class DoubleMLLPQ(NonLinearScoreMixin, DoubleML): diff --git a/doubleml/irm/pq.py b/doubleml/irm/pq.py index 4f83f748a..a5459349e 100644 --- a/doubleml/irm/pq.py +++ b/doubleml/irm/pq.py @@ -1,30 +1,29 @@ import numpy as np from sklearn.base import clone -from sklearn.utils import check_X_y from sklearn.model_selection import StratifiedKFold, train_test_split +from sklearn.utils import check_X_y from ..double_ml import DoubleML -from ..double_ml_score_mixins import NonLinearScoreMixin from ..double_ml_data import DoubleMLData - +from ..double_ml_score_mixins import NonLinearScoreMixin +from ..utils._checks import ( + _check_contains_iv, + _check_quantile, + _check_score, + _check_treatment, + _check_trimming, + _check_zero_one_treatment, +) from ..utils._estimation import ( + _cond_targets, + _default_kde, _dml_cv_predict, - _trimm, - _predict_zero_one_propensity, + _dml_tune, _get_bracket_guess, - _default_kde, _normalize_ipw, - _dml_tune, + _predict_zero_one_propensity, _solve_ipw_score, - _cond_targets, -) -from ..utils._checks import ( - _check_score, - _check_trimming, - _check_zero_one_treatment, - _check_treatment, - _check_contains_iv, - _check_quantile, + _trimm, ) diff --git a/doubleml/irm/qte.py b/doubleml/irm/qte.py index 2a212d77d..43931a5ff 100644 --- a/doubleml/irm/qte.py +++ b/doubleml/irm/qte.py @@ -1,21 +1,17 @@ import numpy as np import pandas as pd - -from sklearn.base import clone - from joblib import Parallel, delayed +from sklearn.base import clone -from ..double_ml_data import DoubleMLData, DoubleMLClusterData -from .pq import DoubleMLPQ -from .lpq import DoubleMLLPQ -from .cvar import DoubleMLCVAR +from ..double_ml_data import DoubleMLClusterData, DoubleMLData from ..double_ml_framework import concat - +from ..utils._checks import _check_sample_splitting, _check_score, _check_trimming, _check_zero_one_treatment +from ..utils._descriptive import generate_summary from ..utils._estimation import _default_kde from ..utils.resampling import DoubleMLResampling -from ..utils._checks import _check_score, _check_trimming, _check_zero_one_treatment, _check_sample_splitting - -from ..utils._descriptive import generate_summary +from .cvar import DoubleMLCVAR +from .lpq import DoubleMLLPQ +from .pq import DoubleMLPQ class DoubleMLQTE: diff --git a/doubleml/irm/ssm.py b/doubleml/irm/ssm.py index c93116c17..5b2362699 100644 --- a/doubleml/irm/ssm.py +++ b/doubleml/irm/ssm.py @@ -1,23 +1,16 @@ -from sklearn.utils import check_X_y -from sklearn.base import clone -from sklearn.model_selection import train_test_split -import numpy as np import copy import warnings +import numpy as np +from sklearn.base import clone +from sklearn.model_selection import train_test_split +from sklearn.utils import check_X_y + from ..double_ml import DoubleML from ..double_ml_data import DoubleMLData -from ..utils._estimation import ( - _trimm, - _dml_cv_predict, - _dml_tune, - _get_cond_smpls_2d, - _predict_zero_one_propensity) -from ..utils._checks import ( - _check_finite_predictions, - _check_trimming, - _check_score) from ..double_ml_score_mixins import LinearScoreMixin +from ..utils._checks import _check_finite_predictions, _check_score, _check_trimming +from ..utils._estimation import _dml_cv_predict, _dml_tune, _get_cond_smpls_2d, _predict_zero_one_propensity, _trimm class DoubleMLSSM(LinearScoreMixin, DoubleML): diff --git a/doubleml/irm/tests/_utils_apo_manual.py b/doubleml/irm/tests/_utils_apo_manual.py index e22f80ffe..fe911b319 100644 --- a/doubleml/irm/tests/_utils_apo_manual.py +++ b/doubleml/irm/tests/_utils_apo_manual.py @@ -1,11 +1,10 @@ import numpy as np from sklearn.base import clone, is_classifier -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, fit_predict_proba, tune_grid_search - -from ...utils._estimation import _normalize_ipw +from ...tests._utils_boot import boot_manual, draw_weights from ...utils._checks import _check_is_propensity +from ...utils._estimation import _normalize_ipw def fit_apo(y, x, d, diff --git a/doubleml/irm/tests/_utils_apos_manual.py b/doubleml/irm/tests/_utils_apos_manual.py index cf47d6450..1677cab9e 100644 --- a/doubleml/irm/tests/_utils_apos_manual.py +++ b/doubleml/irm/tests/_utils_apos_manual.py @@ -1,10 +1,9 @@ import numpy as np from sklearn.base import clone -from ..apo import DoubleMLAPO from ...double_ml_data import DoubleMLData - from ...tests._utils_boot import draw_weights +from ..apo import DoubleMLAPO def fit_apos(y, x, d, diff --git a/doubleml/irm/tests/_utils_cvar_manual.py b/doubleml/irm/tests/_utils_cvar_manual.py index 34f072201..2f523549d 100644 --- a/doubleml/irm/tests/_utils_cvar_manual.py +++ b/doubleml/irm/tests/_utils_cvar_manual.py @@ -1,9 +1,9 @@ import numpy as np from sklearn.base import clone -from sklearn.model_selection import train_test_split, StratifiedKFold +from sklearn.model_selection import StratifiedKFold, train_test_split from ...tests._utils import fit_predict_proba, tune_grid_search -from ...utils._estimation import _dml_cv_predict, _normalize_ipw, _get_bracket_guess, _solve_ipw_score +from ...utils._estimation import _dml_cv_predict, _get_bracket_guess, _normalize_ipw, _solve_ipw_score def fit_cvar(y, x, d, quantile, diff --git a/doubleml/irm/tests/_utils_iivm_manual.py b/doubleml/irm/tests/_utils_iivm_manual.py index d3ebde2cf..f70a18903 100644 --- a/doubleml/irm/tests/_utils_iivm_manual.py +++ b/doubleml/irm/tests/_utils_iivm_manual.py @@ -1,9 +1,8 @@ import numpy as np from sklearn.base import clone, is_classifier -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, fit_predict_proba, tune_grid_search - +from ...tests._utils_boot import boot_manual, draw_weights from ...utils._estimation import _normalize_ipw diff --git a/doubleml/irm/tests/_utils_irm_manual.py b/doubleml/irm/tests/_utils_irm_manual.py index 5fbdd174c..24678db10 100644 --- a/doubleml/irm/tests/_utils_irm_manual.py +++ b/doubleml/irm/tests/_utils_irm_manual.py @@ -1,11 +1,10 @@ import numpy as np from sklearn.base import clone, is_classifier -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, fit_predict_proba, tune_grid_search - -from ...utils._estimation import _normalize_ipw +from ...tests._utils_boot import boot_manual, draw_weights from ...utils._checks import _check_is_propensity +from ...utils._estimation import _normalize_ipw def fit_irm(y, x, d, diff --git a/doubleml/irm/tests/_utils_lpq_manual.py b/doubleml/irm/tests/_utils_lpq_manual.py index ce401f26b..c7a6b9d8a 100644 --- a/doubleml/irm/tests/_utils_lpq_manual.py +++ b/doubleml/irm/tests/_utils_lpq_manual.py @@ -1,10 +1,10 @@ import numpy as np -from sklearn.base import clone -from sklearn.model_selection import train_test_split, StratifiedKFold from scipy.optimize import root_scalar +from sklearn.base import clone +from sklearn.model_selection import StratifiedKFold, train_test_split from ...tests._utils import tune_grid_search -from ...utils._estimation import _dml_cv_predict, _trimm, _default_kde, _normalize_ipw, _get_bracket_guess, _solve_ipw_score +from ...utils._estimation import _default_kde, _dml_cv_predict, _get_bracket_guess, _normalize_ipw, _solve_ipw_score, _trimm def fit_lpq(y, x, d, z, quantile, diff --git a/doubleml/irm/tests/_utils_pq_manual.py b/doubleml/irm/tests/_utils_pq_manual.py index 93d43a4aa..44c3cc6bb 100644 --- a/doubleml/irm/tests/_utils_pq_manual.py +++ b/doubleml/irm/tests/_utils_pq_manual.py @@ -1,10 +1,10 @@ import numpy as np -from sklearn.base import clone -from sklearn.model_selection import train_test_split, StratifiedKFold from scipy.optimize import root_scalar +from sklearn.base import clone +from sklearn.model_selection import StratifiedKFold, train_test_split from ...tests._utils import tune_grid_search -from ...utils._estimation import _dml_cv_predict, _default_kde, _normalize_ipw, _solve_ipw_score, _get_bracket_guess +from ...utils._estimation import _default_kde, _dml_cv_predict, _get_bracket_guess, _normalize_ipw, _solve_ipw_score def fit_pq(y, x, d, quantile, diff --git a/doubleml/irm/tests/_utils_qte_manual.py b/doubleml/irm/tests/_utils_qte_manual.py index 5c177907c..ea742b407 100644 --- a/doubleml/irm/tests/_utils_qte_manual.py +++ b/doubleml/irm/tests/_utils_qte_manual.py @@ -1,11 +1,10 @@ import numpy as np from sklearn.base import clone -from ..pq import DoubleMLPQ from ...double_ml_data import DoubleMLData - from ...tests._utils_boot import draw_weights from ...utils._estimation import _default_kde +from ..pq import DoubleMLPQ def fit_qte(y, x, d, quantiles, learner_g, learner_m, all_smpls, n_rep=1, diff --git a/doubleml/irm/tests/conftest.py b/doubleml/irm/tests/conftest.py index 6fe207b06..4e5b07f99 100644 --- a/doubleml/irm/tests/conftest.py +++ b/doubleml/irm/tests/conftest.py @@ -1,11 +1,10 @@ import numpy as np import pandas as pd - import pytest from scipy.linalg import toeplitz - from sklearn.datasets import make_spd_matrix -from doubleml.datasets import make_irm_data, make_iivm_data + +from doubleml.datasets import make_iivm_data, make_irm_data def _g(x): diff --git a/doubleml/irm/tests/test_apo.py b/doubleml/irm/tests/test_apo.py index 700db40a5..806c7dc5c 100644 --- a/doubleml/irm/tests/test_apo.py +++ b/doubleml/irm/tests/test_apo.py @@ -1,18 +1,17 @@ +import math + import numpy as np import pandas as pd import pytest -import math - from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml -from doubleml.datasets import make_irm_data_discrete_treatments, make_irm_data +from doubleml.datasets import make_irm_data, make_irm_data_discrete_treatments from ...tests._utils import draw_smpls -from ._utils_apo_manual import fit_apo, boot_apo, fit_sensitivity_elements_apo +from ._utils_apo_manual import boot_apo, fit_apo, fit_sensitivity_elements_apo @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_apo_classifier.py b/doubleml/irm/tests/test_apo_classifier.py index 8b7d8a9de..ca5be5ae3 100644 --- a/doubleml/irm/tests/test_apo_classifier.py +++ b/doubleml/irm/tests/test_apo_classifier.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_apo_manual import fit_apo, boot_apo +from ._utils_apo_manual import boot_apo, fit_apo @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_apo_exceptions.py b/doubleml/irm/tests/test_apo_exceptions.py index 31fa6b447..937f8affc 100644 --- a/doubleml/irm/tests/test_apo_exceptions.py +++ b/doubleml/irm/tests/test_apo_exceptions.py @@ -1,12 +1,11 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd +import pytest +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import Lasso, LogisticRegression from doubleml import DoubleMLAPO, DoubleMLData -from doubleml.datasets import make_irm_data_discrete_treatments, make_iivm_data, make_irm_data - -from sklearn.linear_model import Lasso, LogisticRegression -from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from doubleml.datasets import make_iivm_data, make_irm_data, make_irm_data_discrete_treatments n = 100 data_apo = make_irm_data_discrete_treatments(n_obs=n) diff --git a/doubleml/irm/tests/test_apo_external_predictions.py b/doubleml/irm/tests/test_apo_external_predictions.py index a3f77dea1..6c7900788 100644 --- a/doubleml/irm/tests/test_apo_external_predictions.py +++ b/doubleml/irm/tests/test_apo_external_predictions.py @@ -1,12 +1,13 @@ -import pytest -import numpy as np -import pandas as pd import math +import numpy as np +import pandas as pd +import pytest from sklearn.linear_model import LinearRegression, LogisticRegression + from doubleml import DoubleMLAPO, DoubleMLData from doubleml.datasets import make_irm_data_discrete_treatments -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor from ...tests._utils import draw_smpls diff --git a/doubleml/irm/tests/test_apo_tune.py b/doubleml/irm/tests/test_apo_tune.py index dacb569bc..4450325ea 100644 --- a/doubleml/irm/tests/test_apo_tune.py +++ b/doubleml/irm/tests/test_apo_tune.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_apo_manual import fit_apo, boot_apo, tune_nuisance_apo +from ._utils_apo_manual import boot_apo, fit_apo, tune_nuisance_apo @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_apo_weighted_scores.py b/doubleml/irm/tests/test_apo_weighted_scores.py index 5551e5dd0..73733c8f9 100644 --- a/doubleml/irm/tests/test_apo_weighted_scores.py +++ b/doubleml/irm/tests/test_apo_weighted_scores.py @@ -1,13 +1,13 @@ -import pytest import numpy as np - +import pytest from sklearn.base import clone -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression -from ...tests._utils import draw_smpls import doubleml as dml +from ...tests._utils import draw_smpls + @pytest.fixture(scope='module', params=[[LinearRegression(), diff --git a/doubleml/irm/tests/test_apos.py b/doubleml/irm/tests/test_apos.py index 92a372ff1..f4e6af54c 100644 --- a/doubleml/irm/tests/test_apos.py +++ b/doubleml/irm/tests/test_apos.py @@ -1,17 +1,15 @@ import numpy as np import pandas as pd import pytest - from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml -from doubleml.datasets import make_irm_data_discrete_treatments, make_irm_data +from doubleml.datasets import make_irm_data, make_irm_data_discrete_treatments -from ._utils_apos_manual import fit_apos, boot_apos from ...tests._utils import confint_manual +from ._utils_apos_manual import boot_apos, fit_apos @pytest.mark.ci diff --git a/doubleml/irm/tests/test_apos_classfier.py b/doubleml/irm/tests/test_apos_classfier.py index 9c3e7d351..b6c17fcb8 100644 --- a/doubleml/irm/tests/test_apos_classfier.py +++ b/doubleml/irm/tests/test_apos_classfier.py @@ -1,17 +1,15 @@ import numpy as np import pandas as pd import pytest - from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression import doubleml as dml from doubleml.datasets import make_irm_data_discrete_treatments -from ._utils_apos_manual import fit_apos, boot_apos from ...tests._utils import confint_manual +from ._utils_apos_manual import boot_apos, fit_apos @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_apos_exceptions.py b/doubleml/irm/tests/test_apos_exceptions.py index 0c20efe53..ca7e863ba 100644 --- a/doubleml/irm/tests/test_apos_exceptions.py +++ b/doubleml/irm/tests/test_apos_exceptions.py @@ -1,11 +1,10 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd +import pytest +from sklearn.linear_model import Lasso, LogisticRegression from doubleml import DoubleMLAPOS, DoubleMLData -from doubleml.datasets import make_irm_data_discrete_treatments, make_iivm_data - -from sklearn.linear_model import Lasso, LogisticRegression +from doubleml.datasets import make_iivm_data, make_irm_data_discrete_treatments n = 100 data = make_irm_data_discrete_treatments(n_obs=n) diff --git a/doubleml/irm/tests/test_apos_external_predictions.py b/doubleml/irm/tests/test_apos_external_predictions.py index b6a2c8eed..9657a83be 100644 --- a/doubleml/irm/tests/test_apos_external_predictions.py +++ b/doubleml/irm/tests/test_apos_external_predictions.py @@ -1,12 +1,13 @@ -import pytest -import numpy as np -import pandas as pd import math +import numpy as np +import pandas as pd +import pytest from sklearn.linear_model import LinearRegression, LogisticRegression + from doubleml import DoubleMLAPOS, DoubleMLData from doubleml.datasets import make_irm_data_discrete_treatments -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor from ...tests._utils import draw_smpls diff --git a/doubleml/irm/tests/test_apos_weighted_scores.py b/doubleml/irm/tests/test_apos_weighted_scores.py index 3ab8db6af..490f7a1aa 100644 --- a/doubleml/irm/tests/test_apos_weighted_scores.py +++ b/doubleml/irm/tests/test_apos_weighted_scores.py @@ -1,10 +1,9 @@ -import pytest import numpy as np import pandas as pd - +import pytest from sklearn.base import clone -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml from doubleml.datasets import make_irm_data_discrete_treatments diff --git a/doubleml/irm/tests/test_cvar.py b/doubleml/irm/tests/test_cvar.py index 363f8b01b..82927a1ef 100644 --- a/doubleml/irm/tests/test_cvar.py +++ b/doubleml/irm/tests/test_cvar.py @@ -1,12 +1,12 @@ -import numpy as np -import pytest import math -import doubleml as dml - +import numpy as np +import pytest from sklearn.base import clone -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression + +import doubleml as dml from ...tests._utils import draw_smpls from ._utils_cvar_manual import fit_cvar diff --git a/doubleml/irm/tests/test_cvar_tune.py b/doubleml/irm/tests/test_cvar_tune.py index 69f6ad2d0..6d253dcc9 100644 --- a/doubleml/irm/tests/test_cvar_tune.py +++ b/doubleml/irm/tests/test_cvar_tune.py @@ -1,7 +1,7 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor diff --git a/doubleml/irm/tests/test_iivm.py b/doubleml/irm/tests/test_iivm.py index 153efe5d1..8826c64b7 100644 --- a/doubleml/irm/tests/test_iivm.py +++ b/doubleml/irm/tests/test_iivm.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_iivm_manual import fit_iivm, boot_iivm +from ._utils_iivm_manual import boot_iivm, fit_iivm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_iivm_classifier.py b/doubleml/irm/tests/test_iivm_classifier.py index 1d7a72652..4efefde4f 100644 --- a/doubleml/irm/tests/test_iivm_classifier.py +++ b/doubleml/irm/tests/test_iivm_classifier.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_iivm_manual import fit_iivm, boot_iivm +from ._utils_iivm_manual import boot_iivm, fit_iivm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_iivm_external_predictions.py b/doubleml/irm/tests/test_iivm_external_predictions.py index 8db50b859..1b123baea 100644 --- a/doubleml/irm/tests/test_iivm_external_predictions.py +++ b/doubleml/irm/tests/test_iivm_external_predictions.py @@ -1,10 +1,12 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LinearRegression, LogisticRegression -from doubleml import DoubleMLIIVM, DoubleMLData + +from doubleml import DoubleMLData, DoubleMLIIVM from doubleml.datasets import make_iivm_data -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor @pytest.fixture(scope="module", params=[1, 3]) diff --git a/doubleml/irm/tests/test_iivm_subgroups.py b/doubleml/irm/tests/test_iivm_subgroups.py index 84bed1938..7a2e7b256 100644 --- a/doubleml/irm/tests/test_iivm_subgroups.py +++ b/doubleml/irm/tests/test_iivm_subgroups.py @@ -1,15 +1,14 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_iivm_manual import fit_iivm, boot_iivm +from ._utils_iivm_manual import boot_iivm, fit_iivm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_iivm_tune.py b/doubleml/irm/tests/test_iivm_tune.py index 6ce4ef0f9..2ce28d801 100644 --- a/doubleml/irm/tests/test_iivm_tune.py +++ b/doubleml/irm/tests/test_iivm_tune.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_iivm_manual import fit_iivm, boot_iivm, tune_nuisance_iivm +from ._utils_iivm_manual import boot_iivm, fit_iivm, tune_nuisance_iivm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_irm.py b/doubleml/irm/tests/test_irm.py index 17fd7e6a4..9df51c424 100644 --- a/doubleml/irm/tests/test_irm.py +++ b/doubleml/irm/tests/test_irm.py @@ -1,19 +1,18 @@ +import math + import numpy as np import pandas as pd import pytest -import math - from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml from doubleml.datasets import make_irm_data from doubleml.utils.resampling import DoubleMLResampling from ...tests._utils import draw_smpls -from ._utils_irm_manual import fit_irm, boot_irm, fit_sensitivity_elements_irm +from ._utils_irm_manual import boot_irm, fit_irm, fit_sensitivity_elements_irm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_irm_classifier.py b/doubleml/irm/tests/test_irm_classifier.py index 38e5a798b..af3472114 100644 --- a/doubleml/irm/tests/test_irm_classifier.py +++ b/doubleml/irm/tests/test_irm_classifier.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_irm_manual import fit_irm, boot_irm +from ._utils_irm_manual import boot_irm, fit_irm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_irm_external_predictions.py b/doubleml/irm/tests/test_irm_external_predictions.py index a7a2b6249..dabf6c0eb 100644 --- a/doubleml/irm/tests/test_irm_external_predictions.py +++ b/doubleml/irm/tests/test_irm_external_predictions.py @@ -1,10 +1,12 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LinearRegression, LogisticRegression -from doubleml import DoubleMLIRM, DoubleMLData + +from doubleml import DoubleMLData, DoubleMLIRM from doubleml.datasets import make_irm_data -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor @pytest.fixture(scope="module", params=["ATE", "ATTE"]) diff --git a/doubleml/irm/tests/test_irm_tune.py b/doubleml/irm/tests/test_irm_tune.py index 21338bb43..aee7d098f 100644 --- a/doubleml/irm/tests/test_irm_tune.py +++ b/doubleml/irm/tests/test_irm_tune.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LogisticRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_irm_manual import fit_irm, boot_irm, tune_nuisance_irm +from ._utils_irm_manual import boot_irm, fit_irm, tune_nuisance_irm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_irm_weighted_scores.py b/doubleml/irm/tests/test_irm_weighted_scores.py index 4bdd9bd34..a6dd67904 100644 --- a/doubleml/irm/tests/test_irm_weighted_scores.py +++ b/doubleml/irm/tests/test_irm_weighted_scores.py @@ -1,9 +1,8 @@ -import pytest import numpy as np - +import pytest from sklearn.base import clone -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression import doubleml as dml from doubleml.utils._estimation import _normalize_ipw diff --git a/doubleml/irm/tests/test_irm_with_missings.py b/doubleml/irm/tests/test_irm_with_missings.py index 18eac7ef7..d749ba5a5 100644 --- a/doubleml/irm/tests/test_irm_with_missings.py +++ b/doubleml/irm/tests/test_irm_with_missings.py @@ -1,17 +1,17 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone # TODO: Maybe add some learner which cannot handle missings in x and test the exception -from sklearn.linear_model import LogisticRegression, LinearRegression +from sklearn.linear_model import LinearRegression, LogisticRegression from xgboost import XGBClassifier, XGBRegressor import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_irm_manual import fit_irm, boot_irm +from ._utils_irm_manual import boot_irm, fit_irm @pytest.fixture(scope='module', diff --git a/doubleml/irm/tests/test_lpq.py b/doubleml/irm/tests/test_lpq.py index 089d9d5ce..2d29d0ee4 100644 --- a/doubleml/irm/tests/test_lpq.py +++ b/doubleml/irm/tests/test_lpq.py @@ -1,16 +1,16 @@ -import numpy as np -import pytest import math -import doubleml as dml - +import numpy as np +import pytest from sklearn.base import clone from sklearn.linear_model import LogisticRegression from statsmodels.nonparametric.kde import KDEUnivariate +import doubleml as dml + from ...tests._utils import draw_smpls -from ._utils_lpq_manual import fit_lpq from ...utils._estimation import _default_kde +from ._utils_lpq_manual import fit_lpq def custom_kde(u, weights): diff --git a/doubleml/irm/tests/test_lpq_external_predictions.py b/doubleml/irm/tests/test_lpq_external_predictions.py index eb0a468a5..7824d5491 100644 --- a/doubleml/irm/tests/test_lpq_external_predictions.py +++ b/doubleml/irm/tests/test_lpq_external_predictions.py @@ -1,10 +1,13 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LogisticRegression -from doubleml import DoubleMLLPQ, DoubleMLData + +from doubleml import DoubleMLData, DoubleMLLPQ from doubleml.datasets import make_iivm_data from doubleml.utils import DMLDummyClassifier + from ...tests._utils import draw_smpls diff --git a/doubleml/irm/tests/test_lpq_tune.py b/doubleml/irm/tests/test_lpq_tune.py index 7cf1cdf96..18741bd17 100644 --- a/doubleml/irm/tests/test_lpq_tune.py +++ b/doubleml/irm/tests/test_lpq_tune.py @@ -1,7 +1,7 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone from sklearn.ensemble import RandomForestClassifier diff --git a/doubleml/irm/tests/test_pq.py b/doubleml/irm/tests/test_pq.py index 0447daa0b..50fc8c613 100644 --- a/doubleml/irm/tests/test_pq.py +++ b/doubleml/irm/tests/test_pq.py @@ -1,12 +1,12 @@ -import numpy as np -import pytest import math -import doubleml as dml - +import numpy as np +import pytest from sklearn.base import clone -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression + +import doubleml as dml from ...tests._utils import draw_smpls from ._utils_pq_manual import fit_pq diff --git a/doubleml/irm/tests/test_pq_external_predictions.py b/doubleml/irm/tests/test_pq_external_predictions.py index 3bbc0c56b..87c0b03b8 100644 --- a/doubleml/irm/tests/test_pq_external_predictions.py +++ b/doubleml/irm/tests/test_pq_external_predictions.py @@ -1,10 +1,13 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LogisticRegression -from doubleml import DoubleMLPQ, DoubleMLData + +from doubleml import DoubleMLData, DoubleMLPQ from doubleml.datasets import make_irm_data from doubleml.utils import DMLDummyClassifier + from ...tests._utils import draw_smpls diff --git a/doubleml/irm/tests/test_pq_tune.py b/doubleml/irm/tests/test_pq_tune.py index 2459b99ab..90bd06b9b 100644 --- a/doubleml/irm/tests/test_pq_tune.py +++ b/doubleml/irm/tests/test_pq_tune.py @@ -1,7 +1,7 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone from sklearn.ensemble import RandomForestClassifier diff --git a/doubleml/irm/tests/test_qte.py b/doubleml/irm/tests/test_qte.py index 7c7b8c1df..08af9015a 100644 --- a/doubleml/irm/tests/test_qte.py +++ b/doubleml/irm/tests/test_qte.py @@ -1,20 +1,18 @@ +import copy + import numpy as np import pandas as pd import pytest -import copy - -import doubleml as dml - from sklearn.base import clone -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression -from ...tests._utils import draw_smpls, confint_manual -from ._utils_qte_manual import fit_qte, boot_qte - +import doubleml as dml from doubleml.datasets import make_irm_data -from ...utils._estimation import _default_kde +from ...tests._utils import confint_manual, draw_smpls +from ...utils._estimation import _default_kde +from ._utils_qte_manual import boot_qte, fit_qte quantiles = [0.25, 0.5, 0.75] n_quantiles = len(quantiles) diff --git a/doubleml/irm/tests/test_qte_exceptions.py b/doubleml/irm/tests/test_qte_exceptions.py index 0aca62a80..8a8fa6459 100644 --- a/doubleml/irm/tests/test_qte_exceptions.py +++ b/doubleml/irm/tests/test_qte_exceptions.py @@ -1,14 +1,13 @@ -import pytest import numpy as np import pandas as pd +import pytest +from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import Lasso, LogisticRegression -from doubleml import DoubleMLQTE, DoubleMLData +from doubleml import DoubleMLData, DoubleMLQTE from doubleml.datasets import make_irm_data from doubleml.double_ml_data import DoubleMLBaseData -from sklearn.linear_model import Lasso, LogisticRegression -from sklearn.ensemble import RandomForestClassifier - np.random.seed(42) n = 100 dml_data_irm = make_irm_data(n_obs=n) diff --git a/doubleml/irm/tests/test_ssm.py b/doubleml/irm/tests/test_ssm.py index 1419b450f..5e8728383 100644 --- a/doubleml/irm/tests/test_ssm.py +++ b/doubleml/irm/tests/test_ssm.py @@ -1,9 +1,8 @@ -import pytest import math -import numpy as np +import numpy as np +import pytest from sklearn.base import clone - from sklearn.linear_model import LassoCV, LogisticRegressionCV import doubleml as dml diff --git a/doubleml/irm/tests/test_ssm_exceptions.py b/doubleml/irm/tests/test_ssm_exceptions.py index aed1d6bf4..bc6c0504a 100644 --- a/doubleml/irm/tests/test_ssm_exceptions.py +++ b/doubleml/irm/tests/test_ssm_exceptions.py @@ -1,14 +1,13 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd +import pytest +from sklearn.base import BaseEstimator +from sklearn.linear_model import Lasso, LogisticRegression from doubleml import DoubleMLSSM from doubleml.datasets import make_ssm_data from doubleml.double_ml_data import DoubleMLBaseData -from sklearn.linear_model import Lasso, LogisticRegression -from sklearn.base import BaseEstimator - np.random.seed(3141) n = 100 dml_data_mar = make_ssm_data(n_obs=n, mar=True) diff --git a/doubleml/irm/tests/test_ssm_tune.py b/doubleml/irm/tests/test_ssm_tune.py index 1f9e93558..9e6170c14 100644 --- a/doubleml/irm/tests/test_ssm_tune.py +++ b/doubleml/irm/tests/test_ssm_tune.py @@ -1,11 +1,10 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LogisticRegression import doubleml as dml diff --git a/pyproject.toml b/pyproject.toml index a6853b0b0..9d3f0eb86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,11 @@ exclude = [ [tool.ruff.lint] # all rules can be found here: https://beta.ruff.rs/docs/rules/ select = ["E", "F", "W", "I"] -ignore = [] +ignore = [ + # Use `is` and `is not` for type comparisons, or `isinstance()` for + # isinstance checks + "E721", +] [project.urls] Documentation = "https://docs.doubleml.org" From 0251a6f6e919c66f1d7a4fc30b62496386ab4fc3 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:17:25 +0100 Subject: [PATCH 07/28] reformat irm dir --- doubleml/irm/__init__.py | 4 +- doubleml/irm/apo.py | 324 ++++++------ doubleml/irm/apos.py | 244 +++++---- doubleml/irm/cvar.py | 198 +++---- doubleml/irm/iivm.py | 482 +++++++++++------- doubleml/irm/irm.py | 365 ++++++------- doubleml/irm/lpq.py | 15 +- doubleml/irm/pq.py | 45 +- doubleml/irm/qte.py | 154 +++--- doubleml/irm/ssm.py | 386 ++++++++------ doubleml/irm/tests/_utils_apo_manual.py | 187 ++++--- doubleml/irm/tests/_utils_apos_manual.py | 34 +- doubleml/irm/tests/_utils_cvar_manual.py | 71 ++- doubleml/irm/tests/_utils_iivm_manual.py | 337 ++++++++---- doubleml/irm/tests/_utils_irm_manual.py | 275 ++++++---- doubleml/irm/tests/_utils_lpq_manual.py | 167 +++--- doubleml/irm/tests/_utils_pq_manual.py | 70 ++- doubleml/irm/tests/_utils_qte_manual.py | 84 +-- doubleml/irm/tests/_utils_ssm_manual.py | 143 ++++-- doubleml/irm/tests/conftest.py | 175 +++++-- doubleml/irm/tests/test_apo.py | 241 +++++---- doubleml/irm/tests/test_apo_classifier.py | 120 +++-- doubleml/irm/tests/test_apo_exceptions.py | 147 ++++-- .../tests/test_apo_external_predictions.py | 20 +- doubleml/irm/tests/test_apo_tune.py | 156 +++--- .../irm/tests/test_apo_weighted_scores.py | 65 ++- doubleml/irm/tests/test_apos.py | 225 ++++---- doubleml/irm/tests/test_apos_classfier.py | 181 +++---- doubleml/irm/tests/test_apos_exceptions.py | 76 ++- .../tests/test_apos_external_predictions.py | 23 +- .../irm/tests/test_apos_weighted_scores.py | 101 ++-- doubleml/irm/tests/test_cvar.py | 89 ++-- doubleml/irm/tests/test_cvar_tune.py | 129 +++-- doubleml/irm/tests/test_iivm.py | 126 +++-- doubleml/irm/tests/test_iivm_classifier.py | 122 +++-- .../tests/test_iivm_external_predictions.py | 8 +- doubleml/irm/tests/test_iivm_subgroups.py | 167 +++--- doubleml/irm/tests/test_iivm_tune.py | 200 +++++--- doubleml/irm/tests/test_irm.py | 306 ++++++----- doubleml/irm/tests/test_irm_classifier.py | 119 +++-- doubleml/irm/tests/test_irm_tune.py | 127 ++--- .../irm/tests/test_irm_weighted_scores.py | 86 ++-- doubleml/irm/tests/test_irm_with_missings.py | 128 ++--- doubleml/irm/tests/test_lpq.py | 127 +++-- .../tests/test_lpq_external_predictions.py | 5 +- doubleml/irm/tests/test_lpq_tune.py | 163 +++--- doubleml/irm/tests/test_pq.py | 81 +-- .../irm/tests/test_pq_external_predictions.py | 6 +- doubleml/irm/tests/test_pq_tune.py | 126 +++-- doubleml/irm/tests/test_qte.py | 149 +++--- doubleml/irm/tests/test_qte_exceptions.py | 69 +-- doubleml/irm/tests/test_ssm.py | 78 ++- doubleml/irm/tests/test_ssm_exceptions.py | 147 +++--- doubleml/irm/tests/test_ssm_tune.py | 159 +++--- 54 files changed, 4429 insertions(+), 3403 deletions(-) diff --git a/doubleml/irm/__init__.py b/doubleml/irm/__init__.py index 62ecccd0e..a48cfe35b 100644 --- a/doubleml/irm/__init__.py +++ b/doubleml/irm/__init__.py @@ -2,6 +2,4 @@ The :mod:`doubleml.irm` module implements double machine learning estimates based on interactive regression models. """ -__all__ = [ - -] +__all__ = [] diff --git a/doubleml/irm/apo.py b/doubleml/irm/apo.py index e97157286..08ca45112 100644 --- a/doubleml/irm/apo.py +++ b/doubleml/irm/apo.py @@ -78,31 +78,30 @@ class DoubleMLAPO(LinearScoreMixin, DoubleML): Default is ``True``. """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m, - treatment_level, - n_folds=5, - n_rep=1, - score='APO', - weights=None, - normalize_ipw=False, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m, + treatment_level, + n_folds=5, + n_rep=1, + score="APO", + weights=None, + normalize_ipw=False, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) # set up treatment level and check data self._treatment_level = treatment_level self._treated = self._dml_data.d == self._treatment_level self._check_data(self._dml_data) - valid_scores = ['APO'] + valid_scores = ["APO"] _check_score(self.score, valid_scores, allow_callable=False) # set stratication for resampling @@ -110,23 +109,26 @@ def __init__(self, if draw_sample_splitting: self.draw_sample_splitting() - ml_g_is_classifier = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - self._learner = {'ml_g': ml_g, 'ml_m': ml_m} + ml_g_is_classifier = self._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + self._learner = {"ml_g": ml_g, "ml_m": ml_m} if ml_g_is_classifier: if obj_dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba', 'ml_m': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba", "ml_m": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict_proba'} + self._predict_method = {"ml_g": "predict", "ml_m": "predict_proba"} self._initialize_ml_nuisance_params() self._normalize_ipw = normalize_ipw if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) self._trimming_rule = trimming_rule self._trimming_threshold = trimming_threshold _check_trimming(self._trimming_rule, self._trimming_threshold) @@ -181,105 +183,118 @@ def weights(self): return self._weights def _initialize_ml_nuisance_params(self): - valid_learner = ['ml_g0', 'ml_g1', 'ml_m'] - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in valid_learner} + valid_learner = ["ml_g0", "ml_g1", "ml_m"] + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} def _initialize_weights(self, weights): if weights is None: weights = np.ones(self._dml_data.n_obs) if isinstance(weights, np.ndarray): - self._weights = {'weights': weights} + self._weights = {"weights": weights} else: assert isinstance(weights, dict) self._weights = weights def _get_weights(self): # standard case for APO/ATE - weights = self._weights['weights'] - if 'weights_bar' not in self._weights.keys(): - weights_bar = self._weights['weights'] + weights = self._weights["weights"] + if "weights_bar" not in self._weights.keys(): + weights_bar = self._weights["weights"] else: - weights_bar = self._weights['weights_bar'][:, self._i_rep] + weights_bar = self._weights["weights_bar"][:, self._i_rep] return weights, weights_bar def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) # use the treated indicator to get the correct sample splits - x, treated = check_X_y(x, self.treated, - force_all_finite=False) + x, treated = check_X_y(x, self.treated, force_all_finite=False) # get train indices for d == treatment_level smpls_d0, smpls_d1 = _get_cond_smpls(smpls, treated) - g0_external = external_predictions['ml_g0'] is not None - g1_external = external_predictions['ml_g1'] is not None - m_external = external_predictions['ml_m'] is not None + g0_external = external_predictions["ml_g0"] is not None + g1_external = external_predictions["ml_g1"] is not None + m_external = external_predictions["ml_m"] is not None # nuisance g (g0 only relevant for sensitivity analysis) if g0_external: # use external predictions - g_hat0 = {'preds': external_predictions['ml_g0'], - 'targets': _cond_targets(y, cond_sample=(treated == 0)), - 'models': None} + g_hat0 = { + "preds": external_predictions["ml_g0"], + "targets": _cond_targets(y, cond_sample=(treated == 0)), + "models": None, + } else: - g_hat0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g0'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', smpls) - g_hat0['targets'] = _cond_targets(g_hat0['targets'], cond_sample=(treated == 0)) + g_hat0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", smpls) + g_hat0["targets"] = _cond_targets(g_hat0["targets"], cond_sample=(treated == 0)) if self._dml_data.binary_outcome: - _check_binary_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', self._dml_data.y_col) + _check_binary_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", self._dml_data.y_col) if g1_external: # use external predictions - g_hat1 = {'preds': external_predictions['ml_g1'], - 'targets': _cond_targets(y, cond_sample=(treated == 1)), - 'models': None} + g_hat1 = { + "preds": external_predictions["ml_g1"], + "targets": _cond_targets(y, cond_sample=(treated == 1)), + "models": None, + } else: - g_hat1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g1'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', smpls) + g_hat1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", smpls) # adjust target values to consider only compatible subsamples - g_hat1['targets'] = _cond_targets(g_hat1['targets'], cond_sample=(treated == 1)) + g_hat1["targets"] = _cond_targets(g_hat1["targets"], cond_sample=(treated == 1)) if self._dml_data.binary_outcome: - _check_binary_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', self._dml_data.y_col) + _check_binary_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", self._dml_data.y_col) # nuisance m if m_external: # use external predictions - m_hat = {'preds': external_predictions['ml_m'], - 'targets': treated, - 'models': None} + m_hat = {"preds": external_predictions["ml_m"], "targets": treated, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, treated, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) - _check_is_propensity(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls, eps=1e-12) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + treated, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) + _check_is_propensity(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls, eps=1e-12) # also trimm external predictions - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) - - psi_a, psi_b = self._score_elements(y, treated, g_hat0['preds'], g_hat1['preds'], - m_hat['preds'], smpls) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - - preds = {'predictions': {'ml_g0': g_hat0['preds'], - 'ml_g1': g_hat1['preds'], - 'ml_m': m_hat['preds']}, - 'targets': {'ml_g0': g_hat0['targets'], - 'ml_g1': g_hat1['targets'], - 'ml_m': m_hat['targets']}, - 'models': {'ml_g0': g_hat0['models'], - 'ml_g1': g_hat1['models'], - 'ml_m': m_hat['models']} - } + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) + + psi_a, psi_b = self._score_elements(y, treated, g_hat0["preds"], g_hat1["preds"], m_hat["preds"], smpls) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + + preds = { + "predictions": {"ml_g0": g_hat0["preds"], "ml_g1": g_hat1["preds"], "ml_m": m_hat["preds"]}, + "targets": {"ml_g0": g_hat0["targets"], "ml_g1": g_hat1["targets"], "ml_m": m_hat["targets"]}, + "models": {"ml_g0": g_hat0["models"], "ml_g1": g_hat1["models"], "ml_m": m_hat["models"]}, + } return psi_elements, preds def _score_elements(self, y, treated, g_hat0, g_hat1, m_hat, smpls): @@ -300,13 +315,13 @@ def _sensitivity_element_est(self, preds): y = self._dml_data.y treated = self.treated - m_hat = preds['predictions']['ml_m'] - g_hat0 = preds['predictions']['ml_g0'] - g_hat1 = preds['predictions']['ml_g1'] + m_hat = preds["predictions"]["ml_m"] + g_hat0 = preds["predictions"]["ml_g0"] + g_hat1 = preds["predictions"]["ml_g1"] weights, weights_bar = self._get_weights() - sigma2_score_element = np.square(y - np.multiply(treated, g_hat1) - np.multiply(1.0-treated, g_hat0)) + sigma2_score_element = np.square(y - np.multiply(treated, g_hat1) - np.multiply(1.0 - treated, g_hat0)) sigma2 = np.mean(sigma2_score_element) psi_sigma2 = sigma2_score_element - sigma2 @@ -318,77 +333,101 @@ def _sensitivity_element_est(self, preds): nu2 = np.mean(nu2_score_element) psi_nu2 = nu2_score_element - nu2 - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2, - 'riesz_rep': rr, - } + element_dict = { + "sigma2": sigma2, + "nu2": nu2, + "psi_sigma2": psi_sigma2, + "psi_nu2": psi_nu2, + "riesz_rep": rr, + } return element_dict - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, treated = check_X_y(x, self.treated, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, treated = check_X_y(x, self.treated, force_all_finite=False) # get train indices for d == 0 and d == 1 smpls_d0, smpls_d1 = _get_cond_smpls(smpls, treated) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': None} + scoring_methods = {"ml_g": None, "ml_m": None} train_inds = [train_index for (train_index, _) in smpls] train_inds_d0 = [train_index for (train_index, _) in smpls_d0] train_inds_d1 = [train_index for (train_index, _) in smpls_d1] - g0_tune_res = _dml_tune(y, x, train_inds_d0, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - g1_tune_res = _dml_tune(y, x, train_inds_d1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - m_tune_res = _dml_tune(treated, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g0_tune_res = _dml_tune( + y, + x, + train_inds_d0, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + g1_tune_res = _dml_tune( + y, + x, + train_inds_d1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + m_tune_res = _dml_tune( + treated, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g0_best_params = [xx.best_params_ for xx in g0_tune_res] g1_best_params = [xx.best_params_ for xx in g1_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g0': g0_best_params, - 'ml_g1': g1_best_params, - 'ml_m': m_best_params} - tune_res = {'g0_tune': g0_tune_res, - 'g1_tune': g1_tune_res, - 'm_tune': m_tune_res} + params = {"ml_g0": g0_best_params, "ml_g1": g1_best_params, "ml_m": m_best_params} + tune_res = {"g0_tune": g0_tune_res, "g1_tune": g1_tune_res, "m_tune": m_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res def _check_data(self, obj_dml_data): if len(obj_dml_data.d_cols) > 1: - raise ValueError('Only one treatment variable is allowed. ' + - f'Got {len(obj_dml_data.d_cols)} treatment variables.') + raise ValueError( + "Only one treatment variable is allowed. " + f"Got {len(obj_dml_data.d_cols)} treatment variables." + ) if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s).') + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s)." + ) # check if treatment level is valid if np.sum(self.treated) < 5: raise ValueError( - 'The number of treated observations is less than 5. ' + - f'Number of treated observations: {np.sum(self.treated)} for treatment level {self.treatment_level}.' + "The number of treated observations is less than 5. " + + f"Number of treated observations: {np.sum(self.treated)} for treatment level {self.treatment_level}." ) if np.mean(self.treated) <= 0.05: - warnings.warn(f'The proportion of observations with treatment level {self.treatment_level} is less than 5%.' - f' Got {np.mean(self.treated) * 100:.2f}%.') + warnings.warn( + f"The proportion of observations with treatment level {self.treatment_level} is less than 5%." + f" Got {np.mean(self.treated) * 100:.2f}%." + ) return @@ -414,17 +453,15 @@ def capo(self, basis, is_gate=False, **kwargs): model : :class:`doubleML.DoubleMLBLP` Best linear Predictor model. """ - valid_score = ['APO'] + valid_score = ["APO"] if self.score not in valid_score: - raise ValueError('Invalid score ' + self.score + '. ' + - 'Valid score ' + ' or '.join(valid_score) + '.') + raise ValueError("Invalid score " + self.score + ". " + "Valid score " + " or ".join(valid_score) + ".") if self.n_rep != 1: - raise NotImplementedError('Only implemented for one repetition. ' + - f'Number of repetitions is {str(self.n_rep)}.') + raise NotImplementedError("Only implemented for one repetition. " + f"Number of repetitions is {str(self.n_rep)}.") # define the orthogonal signal - orth_signal = self.psi_elements['psi_b'].reshape(-1) + orth_signal = self.psi_elements["psi_b"].reshape(-1) # fit the best linear predictor model = DoubleMLBLP(orth_signal, basis=basis, is_gate=is_gate) model.fit(**kwargs) @@ -450,18 +487,19 @@ def gapo(self, groups, **kwargs): Best linear Predictor model for group average potential outcomes. """ if not isinstance(groups, pd.DataFrame): - raise TypeError('Groups must be of DataFrame type. ' - f'Groups of type {str(type(groups))} was passed.') + raise TypeError("Groups must be of DataFrame type. " f"Groups of type {str(type(groups))} was passed.") if not all(groups.dtypes == bool) or all(groups.dtypes == int): if groups.shape[1] == 1: - groups = pd.get_dummies(groups, prefix='Group', prefix_sep='_') + groups = pd.get_dummies(groups, prefix="Group", prefix_sep="_") else: - raise TypeError('Columns of groups must be of bool type or int type (dummy coded). ' - 'Alternatively, groups should only contain one column.') + raise TypeError( + "Columns of groups must be of bool type or int type (dummy coded). " + "Alternatively, groups should only contain one column." + ) if any(groups.sum(0) <= 5): - warnings.warn('At least one group effect is estimated with less than 6 observations.') + warnings.warn("At least one group effect is estimated with less than 6 observations.") model = self.capo(groups, is_gate=True, **kwargs) return model diff --git a/doubleml/irm/apos.py b/doubleml/irm/apos.py index 1800b7f05..93269dd7c 100644 --- a/doubleml/irm/apos.py +++ b/doubleml/irm/apos.py @@ -17,21 +17,23 @@ class DoubleMLAPOS: - """Double machine learning for interactive regression models with multiple discrete treatments. - """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m, - treatment_levels, - n_folds=5, - n_rep=1, - score='APO', - weights=None, - normalize_ipw=False, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): + """Double machine learning for interactive regression models with multiple discrete treatments.""" + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m, + treatment_levels, + n_folds=5, + n_rep=1, + score="APO", + weights=None, + normalize_ipw=False, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): self._dml_data = obj_dml_data self._is_cluster_data = isinstance(obj_dml_data, DoubleMLClusterData) @@ -50,7 +52,7 @@ def __init__(self, # check score self._score = score - valid_scores = ['APO'] + valid_scores = ["APO"] _check_score(self.score, valid_scores, allow_callable=False) # initialize framework which is constructed after the fit method is called @@ -62,20 +64,23 @@ def __init__(self, _check_trimming(self._trimming_rule, self._trimming_threshold) if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) - ml_g_is_classifier = DoubleML._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - _ = DoubleML._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - self._learner = {'ml_g': clone(ml_g), 'ml_m': clone(ml_m)} + ml_g_is_classifier = DoubleML._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + _ = DoubleML._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + self._learner = {"ml_g": clone(ml_g), "ml_m": clone(ml_m)} if ml_g_is_classifier: if obj_dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba', 'ml_m': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba", "ml_m": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict_proba'} + self._predict_method = {"ml_g": "predict", "ml_m": "predict_proba"} # APO weights _check_weights(weights, score="ATE", n_obs=obj_dml_data.n_obs, n_rep=self.n_rep) @@ -91,10 +96,9 @@ def __init__(self, def __str__(self): class_name = self.__class__.__name__ - header = f'================== {class_name} Object ==================\n' + header = f"================== {class_name} Object ==================\n" fit_summary = str(self.summary) - res = header + \ - '\n------------------ Fit summary ------------------\n' + fit_summary + res = header + "\n------------------ Fit summary ------------------\n" + fit_summary return res @property @@ -256,8 +260,10 @@ def smpls(self): The partition used for cross-fitting. """ if self._smpls is None: - err_msg = ('Sample splitting not specified. Draw samples via .draw_sample splitting(). ' + - 'External samples not implemented yet.') + err_msg = ( + "Sample splitting not specified. Draw samples via .draw_sample splitting(). " + + "External samples not implemented yet." + ) raise ValueError(err_msg) return self._smpls @@ -319,12 +325,11 @@ def summary(self): A summary for the estimated causal effect after calling :meth:`fit`. """ if self.framework is None: - col_names = ['coef', 'std err', 't', 'P>|t|'] + col_names = ["coef", "std err", "t", "P>|t|"] df_summary = pd.DataFrame(columns=col_names) else: ci = self.confint() - df_summary = generate_summary(self.coef, self.se, self.t_stat, - self.pval, ci, self._treatment_levels) + df_summary = generate_summary(self.coef, self.se, self.t_stat, self.pval, ci, self._treatment_levels) return df_summary @property @@ -338,7 +343,7 @@ def sensitivity_summary(self): Summary for the sensitivity analysis. """ if self._framework is None: - raise ValueError('Apply sensitivity_analysis() before sensitivity_summary.') + raise ValueError("Apply sensitivity_analysis() before sensitivity_summary.") else: sensitivity_summary = self._framework.sensitivity_summary return sensitivity_summary @@ -385,14 +390,9 @@ def fit(self, n_jobs_models=None, n_jobs_cv=None, store_predictions=True, store_ ext_pred_dict = None # parallel estimation of the models - parallel = Parallel(n_jobs=n_jobs_models, verbose=0, pre_dispatch='2*n_jobs') + parallel = Parallel(n_jobs=n_jobs_models, verbose=0, pre_dispatch="2*n_jobs") fitted_models = parallel( - delayed(self._fit_model)( - i_level, - n_jobs_cv, - store_predictions, - store_models, - ext_pred_dict) + delayed(self._fit_model)(i_level, n_jobs_cv, store_predictions, store_models, ext_pred_dict) for i_level in range(self.n_treatment_levels) ) @@ -429,14 +429,14 @@ def confint(self, joint=False, level=0.95): """ if self.framework is None: - raise ValueError('Apply fit() before confint().') + raise ValueError("Apply fit() before confint().") df_ci = self.framework.confint(joint=joint, level=level) df_ci.set_index(pd.Index(self._treatment_levels), inplace=True) return df_ci - def bootstrap(self, method='normal', n_rep_boot=500): + def bootstrap(self, method="normal", n_rep_boot=500): """ Multiplier bootstrap for DoubleML models. @@ -454,7 +454,7 @@ def bootstrap(self, method='normal', n_rep_boot=500): self : object """ if self._framework is None: - raise ValueError('Apply fit() before bootstrap().') + raise ValueError("Apply fit() before bootstrap().") self._framework.bootstrap(method=method, n_rep_boot=n_rep_boot) return self @@ -497,19 +497,24 @@ def sensitivity_analysis(self, cf_y=0.03, cf_d=0.03, rho=1.0, level=0.95, null_h """ if self._framework is None: - raise ValueError('Apply fit() before sensitivity_analysis().') - self._framework.sensitivity_analysis( - cf_y=cf_y, - cf_d=cf_d, - rho=rho, - level=level, - null_hypothesis=null_hypothesis - ) + raise ValueError("Apply fit() before sensitivity_analysis().") + self._framework.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, rho=rho, level=level, null_hypothesis=null_hypothesis) return self - def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, null_hypothesis=0.0, - include_scenario=True, benchmarks=None, fill=True, grid_bounds=(0.15, 0.15), grid_size=100): + def sensitivity_plot( + self, + idx_treatment=0, + value="theta", + rho=1.0, + level=0.95, + null_hypothesis=0.0, + include_scenario=True, + benchmarks=None, + fill=True, + grid_bounds=(0.15, 0.15), + grid_size=100, + ): """ Contour plot of the sensivity with respect to latent/confounding variables. @@ -563,7 +568,7 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, Plotly figure of the sensitivity contours. """ if self._framework is None: - raise ValueError('Apply fit() before sensitivity_plot().') + raise ValueError("Apply fit() before sensitivity_plot().") fig = self._framework.sensitivity_plot( idx_treatment=idx_treatment, value=value, @@ -574,7 +579,7 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, benchmarks=benchmarks, fill=fill, grid_bounds=grid_bounds, - grid_size=grid_size + grid_size=grid_size, ) return fig @@ -592,18 +597,20 @@ def sensitivity_benchmark(self, benchmarking_set, fit_args=None): # input checks if self.sensitivity_elements is None: - raise NotImplementedError(f'Sensitivity analysis not yet implemented for {self.__class__.__name__}.') + raise NotImplementedError(f"Sensitivity analysis not yet implemented for {self.__class__.__name__}.") if not isinstance(benchmarking_set, list): - raise TypeError('benchmarking_set must be a list. ' - f'{str(benchmarking_set)} of type {type(benchmarking_set)} was passed.') + raise TypeError( + "benchmarking_set must be a list. " f"{str(benchmarking_set)} of type {type(benchmarking_set)} was passed." + ) if len(benchmarking_set) == 0: - raise ValueError('benchmarking_set must not be empty.') + raise ValueError("benchmarking_set must not be empty.") if not set(benchmarking_set) <= set(x_list_long): - raise ValueError(f"benchmarking_set must be a subset of features {str(self._dml_data.x_cols)}. " - f'{str(benchmarking_set)} was passed.') + raise ValueError( + f"benchmarking_set must be a subset of features {str(self._dml_data.x_cols)}. " + f"{str(benchmarking_set)} was passed." + ) if fit_args is not None and not isinstance(fit_args, dict): - raise TypeError('fit_args must be a dict. ' - f'{str(fit_args)} of type {type(fit_args)} was passed.') + raise TypeError("fit_args must be a dict. " f"{str(fit_args)} of type {type(fit_args)} was passed.") # refit short form of the model x_list_short = [x for x in x_list_long if x not in benchmarking_set] @@ -629,10 +636,9 @@ def draw_sample_splitting(self): ------- self : object """ - obj_dml_resampling = DoubleMLResampling(n_folds=self.n_folds, - n_rep=self.n_rep, - n_obs=self._dml_data.n_obs, - stratify=self._dml_data.d) + obj_dml_resampling = DoubleMLResampling( + n_folds=self.n_folds, n_rep=self.n_rep, n_obs=self._dml_data.n_obs, stratify=self._dml_data.d + ) self._smpls = obj_dml_resampling.split_samples() return self @@ -688,7 +694,8 @@ def set_sample_splitting(self, all_smpls, all_smpls_cluster=None): >>> dml_plr_obj.set_sample_splitting(smpls) """ self._smpls, self._smpls_cluster, self._n_rep, self._n_folds = _check_sample_splitting( - all_smpls, all_smpls_cluster, self._dml_data, self._is_cluster_data) + all_smpls, all_smpls_cluster, self._dml_data, self._is_cluster_data + ) self._modellist = self._initialize_models() @@ -714,16 +721,18 @@ def causal_contrast(self, reference_levels): """ if self.framework is None: - raise ValueError('Apply fit() before causal_contrast().') + raise ValueError("Apply fit() before causal_contrast().") if self.n_treatment_levels == 1: - raise ValueError('Only one treatment level. No causal contrast can be computed.') + raise ValueError("Only one treatment level. No causal contrast can be computed.") is_iterable = isinstance(reference_levels, Iterable) if not is_iterable: reference_levels = [reference_levels] is_treatment_level_subset = set(reference_levels).issubset(set(self.treatment_levels)) if not is_treatment_level_subset: - raise ValueError('Invalid reference_levels. reference_levels has to be an iterable subset of treatment_levels or ' - 'a single treatment level.') + raise ValueError( + "Invalid reference_levels. reference_levels has to be an iterable subset of treatment_levels or " + "a single treatment level." + ) skip_index = [] all_treatment_names = [] @@ -733,10 +742,14 @@ def causal_contrast(self, reference_levels): ref_framework = self.modellist[i_ref_lvl].framework skip_index += [i_ref_lvl] - all_acc_frameworks += [model.framework - ref_framework for i, model in - enumerate(self.modellist) if i not in skip_index] - all_treatment_names += [f"{self.treatment_levels[i]} vs {self.treatment_levels[i_ref_lvl]}" for - i in range(self.n_treatment_levels) if i not in skip_index] + all_acc_frameworks += [ + model.framework - ref_framework for i, model in enumerate(self.modellist) if i not in skip_index + ] + all_treatment_names += [ + f"{self.treatment_levels[i]} vs {self.treatment_levels[i_ref_lvl]}" + for i in range(self.n_treatment_levels) + if i not in skip_index + ] acc = concat(all_acc_frameworks) acc.treatment_names = all_treatment_names @@ -749,8 +762,12 @@ def _fit_model(self, i_level, n_jobs_cv=None, store_predictions=True, store_mode external_predictions = external_predictions_dict[self.treatment_levels[i_level]] else: external_predictions = None - model.fit(n_jobs_cv=n_jobs_cv, store_predictions=store_predictions, store_models=store_models, - external_predictions=external_predictions) + model.fit( + n_jobs_cv=n_jobs_cv, + store_predictions=store_predictions, + store_models=store_models, + external_predictions=external_predictions, + ) return model def _check_treatment_levels(self, treatment_levels): @@ -761,36 +778,44 @@ def _check_treatment_levels(self, treatment_levels): treatment_level_list = [t_lvl for t_lvl in treatment_levels] is_d_subset = set(treatment_level_list).issubset(set(self._all_treatment_levels)) if not is_d_subset: - raise ValueError('Invalid reference_levels. reference_levels has to be an iterable subset or ' - 'a single element of the unique treatment levels in the data.') + raise ValueError( + "Invalid reference_levels. reference_levels has to be an iterable subset or " + "a single element of the unique treatment levels in the data." + ) return treatment_level_list def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData or DoubleMLClusterData type.') + raise TypeError("The data must be of DoubleMLData or DoubleMLClusterData type.") if obj_dml_data.z is not None: - raise ValueError('The data must not contain instrumental variables.') + raise ValueError("The data must not contain instrumental variables.") return def _check_external_predictions(self, external_predictions): expected_keys = self.treatment_levels if not isinstance(external_predictions, dict): - raise TypeError('external_predictions must be a dictionary. ' + - f'Object of type {type(external_predictions)} passed.') + raise TypeError( + "external_predictions must be a dictionary. " + f"Object of type {type(external_predictions)} passed." + ) if not set(external_predictions.keys()).issubset(set(expected_keys)): - raise ValueError('external_predictions must be a subset of all treatment levels. ' + - f'Expected keys: {set(expected_keys)}. ' + - f'Passed keys: {set(external_predictions.keys())}.') + raise ValueError( + "external_predictions must be a subset of all treatment levels. " + + f"Expected keys: {set(expected_keys)}. " + + f"Passed keys: {set(external_predictions.keys())}." + ) - expected_learner_keys = ['ml_g0', 'ml_g1', 'ml_m'] + expected_learner_keys = ["ml_g0", "ml_g1", "ml_m"] for key, value in external_predictions.items(): if not isinstance(value, dict): - raise TypeError(f'external_predictions[{key}] must be a dictionary. ' + - f'Object of type {type(value)} passed.') + raise TypeError( + f"external_predictions[{key}] must be a dictionary. " + f"Object of type {type(value)} passed." + ) if not set(value.keys()).issubset(set(expected_learner_keys)): - raise ValueError(f'external_predictions[{key}] must be a subset of {set(expected_learner_keys)}. ' + - f'Passed keys: {set(value.keys())}.') + raise ValueError( + f"external_predictions[{key}] must be a subset of {set(expected_learner_keys)}. " + + f"Passed keys: {set(value.keys())}." + ) return @@ -799,11 +824,11 @@ def _rename_external_predictions(self, external_predictions): ext_pred_dict = {treatment_level: {d_col: {}} for treatment_level in self.treatment_levels} for treatment_level in self.treatment_levels: if "ml_g1" in external_predictions[treatment_level]: - ext_pred_dict[treatment_level][d_col]['ml_g1'] = external_predictions[treatment_level]['ml_g1'] + ext_pred_dict[treatment_level][d_col]["ml_g1"] = external_predictions[treatment_level]["ml_g1"] if "ml_m" in external_predictions[treatment_level]: - ext_pred_dict[treatment_level][d_col]['ml_m'] = external_predictions[treatment_level]['ml_m'] + ext_pred_dict[treatment_level][d_col]["ml_m"] = external_predictions[treatment_level]["ml_m"] if "ml_g0" in external_predictions[treatment_level]: - ext_pred_dict[treatment_level][d_col]['ml_g0'] = external_predictions[treatment_level]['ml_g0'] + ext_pred_dict[treatment_level][d_col]["ml_g0"] = external_predictions[treatment_level]["ml_g0"] return ext_pred_dict @@ -819,24 +844,21 @@ def _initialize_weights(self, weights): def _initialize_models(self): modellist = [None] * self.n_treatment_levels kwargs = { - 'obj_dml_data': self._dml_data, - 'ml_g': self._learner['ml_g'], - 'ml_m': self._learner['ml_m'], - 'score': self.score, - 'n_folds': self.n_folds, - 'n_rep': self.n_rep, - 'weights': self.weights, - 'trimming_rule': self.trimming_rule, - 'trimming_threshold': self.trimming_threshold, - 'normalize_ipw': self.normalize_ipw, - 'draw_sample_splitting': False + "obj_dml_data": self._dml_data, + "ml_g": self._learner["ml_g"], + "ml_m": self._learner["ml_m"], + "score": self.score, + "n_folds": self.n_folds, + "n_rep": self.n_rep, + "weights": self.weights, + "trimming_rule": self.trimming_rule, + "trimming_threshold": self.trimming_threshold, + "normalize_ipw": self.normalize_ipw, + "draw_sample_splitting": False, } for i_level in range(self.n_treatment_levels): # initialize models for all levels - model = DoubleMLAPO( - treatment_level=self._treatment_levels[i_level], - **kwargs - ) + model = DoubleMLAPO(treatment_level=self._treatment_levels[i_level], **kwargs) # synchronize the sample splitting model.set_sample_splitting(all_smpls=self.smpls) diff --git a/doubleml/irm/cvar.py b/doubleml/irm/cvar.py index 7220b3f7b..a39ea666c 100644 --- a/doubleml/irm/cvar.py +++ b/doubleml/irm/cvar.py @@ -96,38 +96,37 @@ class DoubleMLCVAR(LinearScoreMixin, DoubleML): d 1.591441 0.095781 16.615498 5.382582e-62 1.403715 1.779167 """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m, - treatment=1, - quantile=0.5, - n_folds=5, - n_rep=1, - score='CVaR', - normalize_ipw=True, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + def __init__( + self, + obj_dml_data, + ml_g, + ml_m, + treatment=1, + quantile=0.5, + n_folds=5, + n_rep=1, + score="CVaR", + normalize_ipw=True, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._quantile = quantile self._treatment = treatment self._normalize_ipw = normalize_ipw self._check_data(self._dml_data) - valid_score = ['CVaR'] + valid_score = ["CVaR"] _check_score(self.score, valid_score, allow_callable=False) _check_quantile(self.quantile) _check_treatment(self.treatment) if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) # initialize starting values and bounds self._coef_bounds = (self._dml_data.y.min(), self._dml_data.y.max()) @@ -144,10 +143,10 @@ def __init__(self, self._trimming_threshold = trimming_threshold _check_trimming(self._trimming_rule, self._trimming_threshold) - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - self._learner = {'ml_g': clone(ml_g), 'ml_m': clone(ml_m)} - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict_proba'} + _ = self._check_learner(ml_g, "ml_g", regressor=True, classifier=False) + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + self._learner = {"ml_g": clone(ml_g), "ml_m": clone(ml_m)} + self._predict_method = {"ml_g": "predict", "ml_m": "predict_proba"} self._initialize_ml_nuisance_params() @@ -201,24 +200,23 @@ def _score_elements(self, y, d, g_hat, m_hat, pq_est): return psi_a, psi_b def _initialize_ml_nuisance_params(self): - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in ['ml_g', 'ml_m']} + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in ["ml_g", "ml_m"]} def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # initialize nuisance predictions, targets and models - g_hat = {'models': None, - 'targets': np.full(shape=self._dml_data.n_obs, fill_value=np.nan), - 'preds': np.full(shape=self._dml_data.n_obs, fill_value=np.nan) - } - m_hat = {'models': None, - 'targets': np.full(shape=self._dml_data.n_obs, fill_value=np.nan), - 'preds': np.full(shape=self._dml_data.n_obs, fill_value=np.nan) - } + g_hat = { + "models": None, + "targets": np.full(shape=self._dml_data.n_obs, fill_value=np.nan), + "preds": np.full(shape=self._dml_data.n_obs, fill_value=np.nan), + } + m_hat = { + "models": None, + "targets": np.full(shape=self._dml_data.n_obs, fill_value=np.nan), + "preds": np.full(shape=self._dml_data.n_obs, fill_value=np.nan), + } # initialize models fitted_models = {} @@ -226,8 +224,9 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa # set nuisance model parameters est_params = self._get_params(learner) if est_params is not None: - fitted_models[learner] = [clone(self._learner[learner]).set_params(**est_params[i_fold]) - for i_fold in range(self.n_folds)] + fitted_models[learner] = [ + clone(self._learner[learner]).set_params(**est_params[i_fold]) for i_fold in range(self.n_folds) + ] else: fitted_models[learner] = [clone(self._learner[learner]) for i_fold in range(self.n_folds)] @@ -238,19 +237,21 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa test_inds = smpls[i_fold][1] # start nested crossfitting - train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, - random_state=42, stratify=d[train_inds]) - smpls_prelim = [(train, test) for train, test in - StratifiedKFold(n_splits=self.n_folds).split(X=train_inds_1, y=d[train_inds_1])] + train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, random_state=42, stratify=d[train_inds]) + smpls_prelim = [ + (train, test) + for train, test in StratifiedKFold(n_splits=self.n_folds).split(X=train_inds_1, y=d[train_inds_1]) + ] d_train_1 = d[train_inds_1] y_train_1 = y[train_inds_1] x_train_1 = x[train_inds_1, :] # get a copy of ml_m as a preliminary learner - ml_m_prelim = clone(fitted_models['ml_m'][i_fold]) - m_hat_prelim = _dml_cv_predict(ml_m_prelim, x_train_1, d_train_1, - method='predict_proba', smpls=smpls_prelim)['preds'] + ml_m_prelim = clone(fitted_models["ml_m"][i_fold]) + m_hat_prelim = _dml_cv_predict(ml_m_prelim, x_train_1, d_train_1, method="predict_proba", smpls=smpls_prelim)[ + "preds" + ] m_hat_prelim = _trimm(m_hat_prelim, self.trimming_rule, self.trimming_threshold) @@ -282,62 +283,57 @@ def ipw_score(theta): # only consider values with the right treatment status and fit the model dx_treat_train_2 = x_train_2[d_train_2 == self.treatment, :] g_target_train_2_d = g_target_train_2[d_train_2 == self.treatment] - fitted_models['ml_g'][i_fold].fit(dx_treat_train_2, g_target_train_2_d) + fitted_models["ml_g"][i_fold].fit(dx_treat_train_2, g_target_train_2_d) # predict nuisance values on the test data and the corresponding targets - g_hat['preds'][test_inds] = fitted_models['ml_g'][i_fold].predict(x_test) - g_hat['targets'][test_inds] = g_target[test_inds] + g_hat["preds"][test_inds] = fitted_models["ml_g"][i_fold].predict(x_test) + g_hat["targets"][test_inds] = g_target[test_inds] # refit the propensity score on the whole training set - fitted_models['ml_m'][i_fold].fit(x[train_inds, :], d[train_inds]) - m_hat['preds'][test_inds] = _predict_zero_one_propensity(fitted_models['ml_m'][i_fold], x_test) + fitted_models["ml_m"][i_fold].fit(x[train_inds, :], d[train_inds]) + m_hat["preds"][test_inds] = _predict_zero_one_propensity(fitted_models["ml_m"][i_fold], x_test) # set target for propensity score - m_hat['targets'] = d + m_hat["targets"] = d # set the target for g to be a float and only relevant values - g_hat['targets'] = _cond_targets(g_hat['targets'], cond_sample=(d == self.treatment)) + g_hat["targets"] = _cond_targets(g_hat["targets"], cond_sample=(d == self.treatment)) if return_models: - g_hat['models'] = fitted_models['ml_g'] - m_hat['models'] = fitted_models['ml_m'] + g_hat["models"] = fitted_models["ml_g"] + m_hat["models"] = fitted_models["ml_m"] # clip propensities and normalize ipw weights - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) # this is not done in the score to be equivalent to PQ models if self._normalize_ipw: - m_hat_adj = _normalize_ipw(m_hat['preds'], d) + m_hat_adj = _normalize_ipw(m_hat["preds"], d) else: - m_hat_adj = m_hat['preds'] + m_hat_adj = m_hat["preds"] if self.treatment == 0: m_hat_adj = 1 - m_hat_adj # use the average of the ipw estimates to approximate the potential quantile for U (p.4 Kallus et. al) pq_est = np.mean(ipw_vec) - psi_a, psi_b = self._score_elements(y, d, g_hat['preds'], m_hat_adj, pq_est) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_g': g_hat['preds'], - 'ml_m': m_hat['preds']}, - 'targets': {'ml_g': g_hat['targets'], - 'ml_m': m_hat['targets']}, - 'models': {'ml_g': g_hat['models'], - 'ml_m': m_hat['models']} - } + psi_a, psi_b = self._score_elements(y, d, g_hat["preds"], m_hat_adj, pq_est) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_g": g_hat["preds"], "ml_m": m_hat["preds"]}, + "targets": {"ml_g": g_hat["targets"], "ml_m": m_hat["targets"]}, + "models": {"ml_g": g_hat["models"], "ml_m": m_hat["models"]}, + } return psi_elements, preds - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': None} + scoring_methods = {"ml_g": None, "ml_m": None} train_inds = [train_index for (train_index, _) in smpls] train_inds_treat = [np.intersect1d(np.where(d == self.treatment)[0], train) for train, _ in smpls] @@ -347,31 +343,47 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ g_target_1 = np.ones_like(y) * quantile_approx g_target_2 = (y - self.quantile * quantile_approx) / (1 - self.quantile) g_target_approx = np.max(np.column_stack((g_target_1, g_target_2)), 1) - g_tune_res = _dml_tune(g_target_approx, x, train_inds_treat, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - m_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g_tune_res = _dml_tune( + g_target_approx, + x, + train_inds_treat, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + m_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g_best_params = [xx.best_params_ for xx in g_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g': g_best_params, - 'ml_m': m_best_params} - tune_res = {'g_tune': g_tune_res, - 'm_tune': m_tune_res} + params = {"ml_g": g_best_params, "ml_m": m_best_params} + tune_res = {"g_tune": g_tune_res, "m_tune": m_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) _check_contains_iv(obj_dml_data) _check_zero_one_treatment(self) return diff --git a/doubleml/irm/iivm.py b/doubleml/irm/iivm.py index 68817add2..e023f0076 100644 --- a/doubleml/irm/iivm.py +++ b/doubleml/irm/iivm.py @@ -121,27 +121,26 @@ class DoubleMLIIVM(LinearScoreMixin, DoubleML): \\theta_0 = \\frac{\\mathbb{E}[g_0(1, X)] - \\mathbb{E}[g_0(0,X)]}{\\mathbb{E}[r_0(1, X)] - \\mathbb{E}[r_0(0,X)]}. """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m, - ml_r, - n_folds=5, - n_rep=1, - score='LATE', - subgroups=None, - normalize_ipw=False, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m, + ml_r, + n_folds=5, + n_rep=1, + score="LATE", + subgroups=None, + normalize_ipw=False, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) - valid_scores = ['LATE'] + valid_scores = ["LATE"] _check_score(self.score, valid_scores, allow_callable=True) # set stratication for resampling @@ -149,45 +148,50 @@ def __init__(self, if draw_sample_splitting: self.draw_sample_splitting() - ml_g_is_classifier = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - _ = self._check_learner(ml_r, 'ml_r', regressor=False, classifier=True) - self._learner = {'ml_g': ml_g, 'ml_m': ml_m, 'ml_r': ml_r} + ml_g_is_classifier = self._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + _ = self._check_learner(ml_r, "ml_r", regressor=False, classifier=True) + self._learner = {"ml_g": ml_g, "ml_m": ml_m, "ml_r": ml_r} self._normalize_ipw = normalize_ipw if ml_g_is_classifier: if obj_dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba', 'ml_m': 'predict_proba', 'ml_r': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba", "ml_m": "predict_proba", "ml_r": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict_proba', 'ml_r': 'predict_proba'} + self._predict_method = {"ml_g": "predict", "ml_m": "predict_proba", "ml_r": "predict_proba"} self._initialize_ml_nuisance_params() if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) self._trimming_rule = trimming_rule self._trimming_threshold = trimming_threshold _check_trimming(self._trimming_rule, self._trimming_threshold) if subgroups is None: # this is the default for subgroups; via None to prevent a mutable default argument - subgroups = {'always_takers': True, 'never_takers': True} + subgroups = {"always_takers": True, "never_takers": True} else: if not isinstance(subgroups, dict): - raise TypeError('Invalid subgroups ' + str(subgroups) + '. ' + - 'subgroups must be of type dictionary.') - if (not all(k in subgroups for k in ['always_takers', 'never_takers']))\ - | (not all(k in ['always_takers', 'never_takers'] for k in subgroups)): - raise ValueError('Invalid subgroups ' + str(subgroups) + '. ' + - 'subgroups must be a dictionary with keys always_takers and never_takers.') - if not isinstance(subgroups['always_takers'], bool): - raise TypeError("subgroups['always_takers'] must be True or False. " - f'Got {str(subgroups["always_takers"])}.') - if not isinstance(subgroups['never_takers'], bool): - raise TypeError("subgroups['never_takers'] must be True or False. " - f'Got {str(subgroups["never_takers"])}.') + raise TypeError("Invalid subgroups " + str(subgroups) + ". " + "subgroups must be of type dictionary.") + if (not all(k in subgroups for k in ["always_takers", "never_takers"])) | ( + not all(k in ["always_takers", "never_takers"] for k in subgroups) + ): + raise ValueError( + "Invalid subgroups " + + str(subgroups) + + ". " + + "subgroups must be a dictionary with keys always_takers and never_takers." + ) + if not isinstance(subgroups["always_takers"], bool): + raise TypeError("subgroups['always_takers'] must be True or False. " f'Got {str(subgroups["always_takers"])}.') + if not isinstance(subgroups["never_takers"], bool): + raise TypeError("subgroups['never_takers'] must be True or False. " f'Got {str(subgroups["never_takers"])}.') self.subgroups = subgroups self._external_predictions_implemented = True @@ -213,29 +217,33 @@ def trimming_threshold(self): return self._trimming_threshold def _initialize_ml_nuisance_params(self): - valid_learner = ['ml_g0', 'ml_g1', 'ml_m', 'ml_r0', 'ml_r1'] - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in valid_learner} + valid_learner = ["ml_g0", "ml_g1", "ml_m", "ml_r0", "ml_r1"] + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') - one_treat = (obj_dml_data.n_treat == 1) - binary_treat = (type_of_target(obj_dml_data.d) == 'binary') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) + one_treat = obj_dml_data.n_treat == 1 + binary_treat = type_of_target(obj_dml_data.d) == "binary" zero_one_treat = np.all((np.power(obj_dml_data.d, 2) - obj_dml_data.d) == 0) if not (one_treat & binary_treat & zero_one_treat): - raise ValueError('Incompatible data. ' - 'To fit an IIVM model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') - one_instr = (obj_dml_data.n_instr == 1) - err_msg = ('Incompatible data. ' - 'To fit an IIVM model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as instrumental variable.') + raise ValueError( + "Incompatible data. " + "To fit an IIVM model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) + one_instr = obj_dml_data.n_instr == 1 + err_msg = ( + "Incompatible data. " + "To fit an IIVM model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as instrumental variable." + ) if one_instr: - binary_instr = (type_of_target(obj_dml_data.z) == 'binary') + binary_instr = type_of_target(obj_dml_data.z) == "binary" zero_one_instr = np.all((np.power(obj_dml_data.z, 2) - obj_dml_data.z) == 0) if not (one_instr & binary_instr & zero_one_instr): raise ValueError(err_msg) @@ -244,123 +252,151 @@ def _check_data(self, obj_dml_data): return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, z = check_X_y(x, np.ravel(self._dml_data.z), - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, z = check_X_y(x, np.ravel(self._dml_data.z), force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # get train indices for z == 0 and z == 1 smpls_z0, smpls_z1 = _get_cond_smpls(smpls, z) # nuisance g - if external_predictions['ml_g0'] is not None: - g_hat0 = {'preds': external_predictions['ml_g0'], - 'targets': None, - 'models': None} + if external_predictions["ml_g0"] is not None: + g_hat0 = {"preds": external_predictions["ml_g0"], "targets": None, "models": None} else: - g_hat0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_z0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g0'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', smpls) + g_hat0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_z0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", smpls) # adjust target values to consider only compatible subsamples - g_hat0['targets'] = g_hat0['targets'].astype(float) - g_hat0['targets'][z == 1] = np.nan + g_hat0["targets"] = g_hat0["targets"].astype(float) + g_hat0["targets"][z == 1] = np.nan if self._dml_data.binary_outcome: - _check_binary_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', self._dml_data.y_col) - _check_is_propensity(g_hat0['preds'], self._learner['ml_g'], 'ml_g', smpls, eps=1e-12) + _check_binary_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", self._dml_data.y_col) + _check_is_propensity(g_hat0["preds"], self._learner["ml_g"], "ml_g", smpls, eps=1e-12) - if external_predictions['ml_g1'] is not None: - g_hat1 = {'preds': external_predictions['ml_g1'], - 'targets': None, - 'models': None} + if external_predictions["ml_g1"] is not None: + g_hat1 = {"preds": external_predictions["ml_g1"], "targets": None, "models": None} else: - g_hat1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_z1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g1'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', smpls) + g_hat1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_z1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", smpls) # adjust target values to consider only compatible subsamples - g_hat1['targets'] = g_hat1['targets'].astype(float) - g_hat1['targets'][z == 0] = np.nan + g_hat1["targets"] = g_hat1["targets"].astype(float) + g_hat1["targets"][z == 0] = np.nan if self._dml_data.binary_outcome: - _check_binary_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', self._dml_data.y_col) - _check_is_propensity(g_hat1['preds'], self._learner['ml_g'], 'ml_g', smpls, eps=1e-12) + _check_binary_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", self._dml_data.y_col) + _check_is_propensity(g_hat1["preds"], self._learner["ml_g"], "ml_g", smpls, eps=1e-12) # nuisance m - if external_predictions['ml_m'] is not None: - m_hat = {'preds': external_predictions['ml_m'], - 'targets': None, - 'models': None} + if external_predictions["ml_m"] is not None: + m_hat = {"preds": external_predictions["ml_m"], "targets": None, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, z, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) - _check_is_propensity(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls, eps=1e-12) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + z, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) + _check_is_propensity(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls, eps=1e-12) # also trimm external predictions - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) # nuisance r - r0 = external_predictions['ml_r0'] is not None - if self.subgroups['always_takers']: + r0 = external_predictions["ml_r0"] is not None + if self.subgroups["always_takers"]: if r0: - r_hat0 = {'preds': external_predictions['ml_r0'], - 'targets': None, - 'models': None} + r_hat0 = {"preds": external_predictions["ml_r0"], "targets": None, "models": None} else: - r_hat0 = _dml_cv_predict(self._learner['ml_r'], x, d, smpls=smpls_z0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_r0'), method=self._predict_method['ml_r'], - return_models=return_models) + r_hat0 = _dml_cv_predict( + self._learner["ml_r"], + x, + d, + smpls=smpls_z0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_r0"), + method=self._predict_method["ml_r"], + return_models=return_models, + ) else: - r_hat0 = {'preds': np.zeros_like(d), 'targets': np.zeros_like(d), 'models': None} + r_hat0 = {"preds": np.zeros_like(d), "targets": np.zeros_like(d), "models": None} if not r0: - _check_finite_predictions(r_hat0['preds'], self._learner['ml_r'], 'ml_r', smpls) + _check_finite_predictions(r_hat0["preds"], self._learner["ml_r"], "ml_r", smpls) # adjust target values to consider only compatible subsamples - r_hat0['targets'] = r_hat0['targets'].astype(float) - r_hat0['targets'][z == 1] = np.nan + r_hat0["targets"] = r_hat0["targets"].astype(float) + r_hat0["targets"][z == 1] = np.nan - r1 = external_predictions['ml_r1'] is not None - if self.subgroups['never_takers']: + r1 = external_predictions["ml_r1"] is not None + if self.subgroups["never_takers"]: if r1: - r_hat1 = {'preds': external_predictions['ml_r1'], - 'targets': None, - 'models': None} + r_hat1 = {"preds": external_predictions["ml_r1"], "targets": None, "models": None} else: - r_hat1 = _dml_cv_predict(self._learner['ml_r'], x, d, smpls=smpls_z1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_r1'), method=self._predict_method['ml_r'], - return_models=return_models) + r_hat1 = _dml_cv_predict( + self._learner["ml_r"], + x, + d, + smpls=smpls_z1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_r1"), + method=self._predict_method["ml_r"], + return_models=return_models, + ) else: - r_hat1 = {'preds': np.ones_like(d), 'targets': np.ones_like(d), 'models': None} + r_hat1 = {"preds": np.ones_like(d), "targets": np.ones_like(d), "models": None} if not r1: - _check_finite_predictions(r_hat1['preds'], self._learner['ml_r'], 'ml_r', smpls) + _check_finite_predictions(r_hat1["preds"], self._learner["ml_r"], "ml_r", smpls) # adjust target values to consider only compatible subsamples - r_hat1['targets'] = r_hat1['targets'].astype(float) - r_hat1['targets'][z == 0] = np.nan - - psi_a, psi_b = self._score_elements(y, z, d, - g_hat0['preds'], g_hat1['preds'], m_hat['preds'], - r_hat0['preds'], r_hat1['preds'], smpls) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_g0': g_hat0['preds'], - 'ml_g1': g_hat1['preds'], - 'ml_m': m_hat['preds'], - 'ml_r0': r_hat0['preds'], - 'ml_r1': r_hat1['preds']}, - 'targets': {'ml_g0': g_hat0['targets'], - 'ml_g1': g_hat1['targets'], - 'ml_m': m_hat['targets'], - 'ml_r0': r_hat0['targets'], - 'ml_r1': r_hat1['targets']}, - 'models': {'ml_g0': g_hat0['models'], - 'ml_g1': g_hat1['models'], - 'ml_m': m_hat['models'], - 'ml_r0': r_hat0['models'], - 'ml_r1': r_hat1['models']} - } + r_hat1["targets"] = r_hat1["targets"].astype(float) + r_hat1["targets"][z == 0] = np.nan + + psi_a, psi_b = self._score_elements( + y, z, d, g_hat0["preds"], g_hat1["preds"], m_hat["preds"], r_hat0["preds"], r_hat1["preds"], smpls + ) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": { + "ml_g0": g_hat0["preds"], + "ml_g1": g_hat1["preds"], + "ml_m": m_hat["preds"], + "ml_r0": r_hat0["preds"], + "ml_r1": r_hat1["preds"], + }, + "targets": { + "ml_g0": g_hat0["targets"], + "ml_g1": g_hat1["targets"], + "ml_m": m_hat["targets"], + "ml_r0": r_hat0["targets"], + "ml_r1": r_hat1["targets"], + }, + "models": { + "ml_g0": g_hat0["models"], + "ml_g1": g_hat1["models"], + "ml_m": m_hat["models"], + "ml_r0": r_hat0["models"], + "ml_r1": r_hat1["models"], + }, + } return psi_elements, preds @@ -377,64 +413,111 @@ def _score_elements(self, y, z, d, g_hat0, g_hat1, m_hat, r_hat0, r_hat1, smpls) m_hat_adj = m_hat if isinstance(self.score, str): - assert self.score == 'LATE' - psi_b = g_hat1 - g_hat0 \ - + np.divide(np.multiply(z, u_hat1), m_hat_adj) \ - - np.divide(np.multiply(1.0-z, u_hat0), 1.0 - m_hat_adj) - psi_a = -1*(r_hat1 - r_hat0 - + np.divide(np.multiply(z, w_hat1), m_hat_adj) - - np.divide(np.multiply(1.0-z, w_hat0), 1.0 - m_hat_adj)) + assert self.score == "LATE" + psi_b = ( + g_hat1 + - g_hat0 + + np.divide(np.multiply(z, u_hat1), m_hat_adj) + - np.divide(np.multiply(1.0 - z, u_hat0), 1.0 - m_hat_adj) + ) + psi_a = -1 * ( + r_hat1 + - r_hat0 + + np.divide(np.multiply(z, w_hat1), m_hat_adj) + - np.divide(np.multiply(1.0 - z, w_hat0), 1.0 - m_hat_adj) + ) else: assert callable(self.score) - psi_a, psi_b = self.score(y=y, z=z, d=d, - g_hat0=g_hat0, g_hat1=g_hat1, m_hat=m_hat_adj, r_hat0=r_hat0, r_hat1=r_hat1, - smpls=smpls) + psi_a, psi_b = self.score( + y=y, z=z, d=d, g_hat0=g_hat0, g_hat1=g_hat1, m_hat=m_hat_adj, r_hat0=r_hat0, r_hat1=r_hat1, smpls=smpls + ) return psi_a, psi_b - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, z = check_X_y(x, np.ravel(self._dml_data.z), - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, z = check_X_y(x, np.ravel(self._dml_data.z), force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # get train indices for z == 0 and z == 1 smpls_z0, smpls_z1 = _get_cond_smpls(smpls, z) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': None, - 'ml_r': None} + scoring_methods = {"ml_g": None, "ml_m": None, "ml_r": None} train_inds = [train_index for (train_index, _) in smpls] train_inds_z0 = [train_index for (train_index, _) in smpls_z0] train_inds_z1 = [train_index for (train_index, _) in smpls_z1] - g0_tune_res = _dml_tune(y, x, train_inds_z0, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - g1_tune_res = _dml_tune(y, x, train_inds_z1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - m_tune_res = _dml_tune(z, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - if self.subgroups['always_takers']: - r0_tune_res = _dml_tune(d, x, train_inds_z0, - self._learner['ml_r'], param_grids['ml_r'], scoring_methods['ml_r'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g0_tune_res = _dml_tune( + y, + x, + train_inds_z0, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + g1_tune_res = _dml_tune( + y, + x, + train_inds_z1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + m_tune_res = _dml_tune( + z, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + if self.subgroups["always_takers"]: + r0_tune_res = _dml_tune( + d, + x, + train_inds_z0, + self._learner["ml_r"], + param_grids["ml_r"], + scoring_methods["ml_r"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) r0_best_params = [xx.best_params_ for xx in r0_tune_res] else: r0_tune_res = None r0_best_params = [None] * len(smpls) - if self.subgroups['never_takers']: - r1_tune_res = _dml_tune(d, x, train_inds_z1, - self._learner['ml_r'], param_grids['ml_r'], scoring_methods['ml_r'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + if self.subgroups["never_takers"]: + r1_tune_res = _dml_tune( + d, + x, + train_inds_z1, + self._learner["ml_r"], + param_grids["ml_r"], + scoring_methods["ml_r"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) r1_best_params = [xx.best_params_ for xx in r1_tune_res] else: r1_tune_res = None @@ -444,20 +527,23 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ g1_best_params = [xx.best_params_ for xx in g1_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g0': g0_best_params, - 'ml_g1': g1_best_params, - 'ml_m': m_best_params, - 'ml_r0': r0_best_params, - 'ml_r1': r1_best_params} - - tune_res = {'g0_tune': g0_tune_res, - 'g1_tune': g1_tune_res, - 'm_tune': m_tune_res, - 'r0_tune': r0_tune_res, - 'r1_tune': r1_tune_res} - - res = {'params': params, - 'tune_res': tune_res} + params = { + "ml_g0": g0_best_params, + "ml_g1": g1_best_params, + "ml_m": m_best_params, + "ml_r0": r0_best_params, + "ml_r1": r1_best_params, + } + + tune_res = { + "g0_tune": g0_tune_res, + "g1_tune": g1_tune_res, + "m_tune": m_tune_res, + "r0_tune": r0_tune_res, + "r1_tune": r1_tune_res, + } + + res = {"params": params, "tune_res": tune_res} return res diff --git a/doubleml/irm/irm.py b/doubleml/irm/irm.py index dbacac367..9c0103b2a 100644 --- a/doubleml/irm/irm.py +++ b/doubleml/irm/irm.py @@ -119,26 +119,25 @@ class DoubleMLIRM(LinearScoreMixin, DoubleML): \\theta_0 = \\mathbb{E}[g_0(1, X) - g_0(0,X) | D=1]. """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m, - n_folds=5, - n_rep=1, - score='ATE', - weights=None, - normalize_ipw=False, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m, + n_folds=5, + n_rep=1, + score="ATE", + weights=None, + normalize_ipw=False, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) - valid_scores = ['ATE', 'ATTE'] + valid_scores = ["ATE", "ATTE"] _check_score(self.score, valid_scores, allow_callable=True) # set stratication for resampling @@ -146,23 +145,26 @@ def __init__(self, if draw_sample_splitting: self.draw_sample_splitting() - ml_g_is_classifier = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - self._learner = {'ml_g': ml_g, 'ml_m': ml_m} + ml_g_is_classifier = self._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + self._learner = {"ml_g": ml_g, "ml_m": ml_m} if ml_g_is_classifier: if obj_dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba', 'ml_m': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba", "ml_m": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict_proba'} + self._predict_method = {"ml_g": "predict", "ml_m": "predict_proba"} self._initialize_ml_nuisance_params() self._normalize_ipw = normalize_ipw if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) self._trimming_rule = trimming_rule self._trimming_threshold = trimming_threshold _check_trimming(self._trimming_rule, self._trimming_threshold) @@ -202,133 +204,138 @@ def weights(self): return self._weights def _initialize_ml_nuisance_params(self): - valid_learner = ['ml_g0', 'ml_g1', 'ml_m'] - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in valid_learner} + valid_learner = ["ml_g0", "ml_g1", "ml_m"] + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} def _initialize_weights(self, weights): if weights is None: weights = np.ones(self._dml_data.n_obs) if isinstance(weights, np.ndarray): - self._weights = {'weights': weights} + self._weights = {"weights": weights} else: assert isinstance(weights, dict) self._weights = weights def _get_weights(self, m_hat=None): # standard case for ATE - if self.score == 'ATE': - weights = self._weights['weights'] - if 'weights_bar' not in self._weights.keys(): - weights_bar = self._weights['weights'] + if self.score == "ATE": + weights = self._weights["weights"] + if "weights_bar" not in self._weights.keys(): + weights_bar = self._weights["weights"] else: - weights_bar = self._weights['weights_bar'][:, self._i_rep] + weights_bar = self._weights["weights_bar"][:, self._i_rep] else: # special case for ATTE - assert self.score == 'ATTE' + assert self.score == "ATTE" assert m_hat is not None - subgroup = self._weights['weights'] * self._dml_data.d + subgroup = self._weights["weights"] * self._dml_data.d subgroup_probability = np.mean(subgroup) weights = np.divide(subgroup, subgroup_probability) - weights_bar = np.divide( - np.multiply(m_hat, self._weights['weights']), - subgroup_probability) + weights_bar = np.divide(np.multiply(m_hat, self._weights["weights"]), subgroup_probability) return weights, weights_bar def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ' - 'To fit an interactive IV regression model use DoubleMLIIVM instead of DoubleMLIRM.') - one_treat = (obj_dml_data.n_treat == 1) - binary_treat = (type_of_target(obj_dml_data.d) == 'binary') + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + "To fit an interactive IV regression model use DoubleMLIIVM instead of DoubleMLIRM." + ) + one_treat = obj_dml_data.n_treat == 1 + binary_treat = type_of_target(obj_dml_data.d) == "binary" zero_one_treat = np.all((np.power(obj_dml_data.d, 2) - obj_dml_data.d) == 0) if not (one_treat & binary_treat & zero_one_treat): - raise ValueError('Incompatible data. ' - 'To fit an IRM model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + raise ValueError( + "Incompatible data. " + "To fit an IRM model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # get train indices for d == 0 and d == 1 smpls_d0, smpls_d1 = _get_cond_smpls(smpls, d) - g0_external = external_predictions['ml_g0'] is not None - g1_external = external_predictions['ml_g1'] is not None - m_external = external_predictions['ml_m'] is not None + g0_external = external_predictions["ml_g0"] is not None + g1_external = external_predictions["ml_g1"] is not None + m_external = external_predictions["ml_m"] is not None # nuisance g if g0_external: # use external predictions - g_hat0 = {'preds': external_predictions['ml_g0'], - 'targets': None, - 'models': None} + g_hat0 = {"preds": external_predictions["ml_g0"], "targets": None, "models": None} else: - g_hat0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d0, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g0'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', smpls) - g_hat0['targets'] = _cond_targets(g_hat0['targets'], cond_sample=(d == 0)) + g_hat0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d0, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", smpls) + g_hat0["targets"] = _cond_targets(g_hat0["targets"], cond_sample=(d == 0)) if self._dml_data.binary_outcome: - _check_binary_predictions(g_hat0['preds'], self._learner['ml_g'], 'ml_g', self._dml_data.y_col) + _check_binary_predictions(g_hat0["preds"], self._learner["ml_g"], "ml_g", self._dml_data.y_col) if g1_external: # use external predictions - g_hat1 = {'preds': external_predictions['ml_g1'], - 'targets': None, - 'models': None} + g_hat1 = {"preds": external_predictions["ml_g1"], "targets": None, "models": None} else: - g_hat1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g1'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', smpls) + g_hat1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", smpls) # adjust target values to consider only compatible subsamples - g_hat1['targets'] = _cond_targets(g_hat1['targets'], cond_sample=(d == 1)) + g_hat1["targets"] = _cond_targets(g_hat1["targets"], cond_sample=(d == 1)) - if self._dml_data.binary_outcome & (self.score != 'ATTE'): - _check_binary_predictions(g_hat1['preds'], self._learner['ml_g'], 'ml_g', self._dml_data.y_col) + if self._dml_data.binary_outcome & (self.score != "ATTE"): + _check_binary_predictions(g_hat1["preds"], self._learner["ml_g"], "ml_g", self._dml_data.y_col) # nuisance m if m_external: # use external predictions - m_hat = {'preds': external_predictions['ml_m'], - 'targets': None, - 'models': None} + m_hat = {"preds": external_predictions["ml_m"], "targets": None, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) - _check_is_propensity(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls, eps=1e-12) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) + _check_is_propensity(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls, eps=1e-12) # also trimm external predictions - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) - - psi_a, psi_b = self._score_elements(y, d, - g_hat0['preds'], g_hat1['preds'], m_hat['preds'], - smpls) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_g0': g_hat0['preds'], - 'ml_g1': g_hat1['preds'], - 'ml_m': m_hat['preds']}, - 'targets': {'ml_g0': g_hat0['targets'], - 'ml_g1': g_hat1['targets'], - 'ml_m': m_hat['targets']}, - 'models': {'ml_g0': g_hat0['models'], - 'ml_g1': g_hat1['models'], - 'ml_m': m_hat['models']} - } + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) + + psi_a, psi_b = self._score_elements(y, d, g_hat0["preds"], g_hat1["preds"], m_hat["preds"], smpls) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_g0": g_hat0["preds"], "ml_g1": g_hat1["preds"], "ml_m": m_hat["preds"]}, + "targets": {"ml_g0": g_hat0["targets"], "ml_g1": g_hat1["targets"], "ml_m": m_hat["targets"]}, + "models": {"ml_g0": g_hat0["models"], "ml_g1": g_hat1["models"], "ml_m": m_hat["models"]}, + } return psi_elements, preds @@ -343,22 +350,19 @@ def _score_elements(self, y, d, g_hat0, g_hat1, m_hat, smpls): u_hat0 = y - g_hat0 u_hat1 = y - g_hat1 - if (self.score == 'ATE') or (self.score == 'ATTE'): + if (self.score == "ATE") or (self.score == "ATTE"): weights, weights_bar = self._get_weights(m_hat=m_hat_adj) - psi_b = weights * (g_hat1 - g_hat0) \ - + weights_bar * ( - np.divide(np.multiply(d, u_hat1), m_hat_adj) - - np.divide(np.multiply(1.0-d, u_hat0), 1.0 - m_hat_adj)) - if self.score == 'ATE': + psi_b = weights * (g_hat1 - g_hat0) + weights_bar * ( + np.divide(np.multiply(d, u_hat1), m_hat_adj) - np.divide(np.multiply(1.0 - d, u_hat0), 1.0 - m_hat_adj) + ) + if self.score == "ATE": psi_a = np.full_like(m_hat_adj, -1.0) else: - assert self.score == 'ATTE' + assert self.score == "ATTE" psi_a = -1.0 * weights else: assert callable(self.score) - psi_a, psi_b = self.score(y=y, d=d, - g_hat0=g_hat0, g_hat1=g_hat1, m_hat=m_hat_adj, - smpls=smpls) + psi_a, psi_b = self.score(y=y, d=d, g_hat0=g_hat0, g_hat1=g_hat1, m_hat=m_hat_adj, smpls=smpls) return psi_a, psi_b @@ -367,73 +371,94 @@ def _sensitivity_element_est(self, preds): y = self._dml_data.y d = self._dml_data.d - m_hat = preds['predictions']['ml_m'] - g_hat0 = preds['predictions']['ml_g0'] - g_hat1 = preds['predictions']['ml_g1'] + m_hat = preds["predictions"]["ml_m"] + g_hat0 = preds["predictions"]["ml_g0"] + g_hat1 = preds["predictions"]["ml_g1"] # use weights make this extendable weights, weights_bar = self._get_weights(m_hat=m_hat) - sigma2_score_element = np.square(y - np.multiply(d, g_hat1) - np.multiply(1.0-d, g_hat0)) + sigma2_score_element = np.square(y - np.multiply(d, g_hat1) - np.multiply(1.0 - d, g_hat0)) sigma2 = np.mean(sigma2_score_element) psi_sigma2 = sigma2_score_element - sigma2 # calc m(W,alpha) and Riesz representer - m_alpha = np.multiply(weights, np.multiply(weights_bar, (np.divide(1.0, m_hat) + np.divide(1.0, 1.0-m_hat)))) - rr = np.multiply(weights_bar, (np.divide(d, m_hat) - np.divide(1.0-d, 1.0-m_hat))) + m_alpha = np.multiply(weights, np.multiply(weights_bar, (np.divide(1.0, m_hat) + np.divide(1.0, 1.0 - m_hat)))) + rr = np.multiply(weights_bar, (np.divide(d, m_hat) - np.divide(1.0 - d, 1.0 - m_hat))) nu2_score_element = np.multiply(2.0, m_alpha) - np.square(rr) nu2 = np.mean(nu2_score_element) psi_nu2 = nu2_score_element - nu2 - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2, - 'riesz_rep': rr, - } + element_dict = { + "sigma2": sigma2, + "nu2": nu2, + "psi_sigma2": psi_sigma2, + "psi_nu2": psi_nu2, + "riesz_rep": rr, + } return element_dict - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # get train indices for d == 0 and d == 1 smpls_d0, smpls_d1 = _get_cond_smpls(smpls, d) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': None} + scoring_methods = {"ml_g": None, "ml_m": None} train_inds = [train_index for (train_index, _) in smpls] train_inds_d0 = [train_index for (train_index, _) in smpls_d0] train_inds_d1 = [train_index for (train_index, _) in smpls_d1] - g0_tune_res = _dml_tune(y, x, train_inds_d0, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - g1_tune_res = _dml_tune(y, x, train_inds_d1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - m_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g0_tune_res = _dml_tune( + y, + x, + train_inds_d0, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + g1_tune_res = _dml_tune( + y, + x, + train_inds_d1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + m_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g0_best_params = [xx.best_params_ for xx in g0_tune_res] g1_best_params = [xx.best_params_ for xx in g1_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g0': g0_best_params, - 'ml_g1': g1_best_params, - 'ml_m': m_best_params} - tune_res = {'g0_tune': g0_tune_res, - 'g1_tune': g1_tune_res, - 'm_tune': m_tune_res} + params = {"ml_g0": g0_best_params, "ml_g1": g1_best_params, "ml_m": m_best_params} + tune_res = {"g0_tune": g0_tune_res, "g1_tune": g1_tune_res, "m_tune": m_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res @@ -459,17 +484,15 @@ def cate(self, basis, is_gate=False, **kwargs): model : :class:`doubleML.DoubleMLBLP` Best linear Predictor model. """ - valid_score = ['ATE'] + valid_score = ["ATE"] if self.score not in valid_score: - raise ValueError('Invalid score ' + self.score + '. ' + - 'Valid score ' + ' or '.join(valid_score) + '.') + raise ValueError("Invalid score " + self.score + ". " + "Valid score " + " or ".join(valid_score) + ".") if self.n_rep != 1: - raise NotImplementedError('Only implemented for one repetition. ' + - f'Number of repetitions is {str(self.n_rep)}.') + raise NotImplementedError("Only implemented for one repetition. " + f"Number of repetitions is {str(self.n_rep)}.") # define the orthogonal signal - orth_signal = self.psi_elements['psi_b'].reshape(-1) + orth_signal = self.psi_elements["psi_b"].reshape(-1) # fit the best linear predictor model = DoubleMLBLP(orth_signal, basis=basis, is_gate=is_gate) model.fit(**kwargs) @@ -495,18 +518,19 @@ def gate(self, groups, **kwargs): Best linear Predictor model for Group Effects. """ if not isinstance(groups, pd.DataFrame): - raise TypeError('Groups must be of DataFrame type. ' - f'Groups of type {str(type(groups))} was passed.') + raise TypeError("Groups must be of DataFrame type. " f"Groups of type {str(type(groups))} was passed.") if not all(groups.dtypes == bool) or all(groups.dtypes == int): if groups.shape[1] == 1: - groups = pd.get_dummies(groups, prefix='Group', prefix_sep='_') + groups = pd.get_dummies(groups, prefix="Group", prefix_sep="_") else: - raise TypeError('Columns of groups must be of bool type or int type (dummy coded). ' - 'Alternatively, groups should only contain one column.') + raise TypeError( + "Columns of groups must be of bool type or int type (dummy coded). " + "Alternatively, groups should only contain one column." + ) if any(groups.sum(0) <= 5): - warnings.warn('At least one group effect is estimated with less than 6 observations.') + warnings.warn("At least one group effect is estimated with less than 6 observations.") model = self.cate(groups, is_gate=True, **kwargs) return model @@ -536,22 +560,19 @@ def policy_tree(self, features, depth=2, **tree_params): model : :class:`doubleML.DoubleMLPolicyTree` Policy tree model. """ - valid_score = ['ATE'] + valid_score = ["ATE"] if self.score not in valid_score: - raise ValueError('Invalid score ' + self.score + '. ' + - 'Valid score ' + ' or '.join(valid_score) + '.') + raise ValueError("Invalid score " + self.score + ". " + "Valid score " + " or ".join(valid_score) + ".") if self.n_rep != 1: - raise NotImplementedError('Only implemented for one repetition. ' + - f'Number of repetitions is {str(self.n_rep)}.') + raise NotImplementedError("Only implemented for one repetition. " + f"Number of repetitions is {str(self.n_rep)}.") _check_integer(depth, "Depth", 0) if not isinstance(features, pd.DataFrame): - raise TypeError('Covariates must be of DataFrame type. ' - f'Covariates of type {str(type(features))} was passed.') + raise TypeError("Covariates must be of DataFrame type. " f"Covariates of type {str(type(features))} was passed.") - orth_signal = self.psi_elements['psi_b'].reshape(-1) + orth_signal = self.psi_elements["psi_b"].reshape(-1) model = DoubleMLPolicyTree(orth_signal, depth=depth, features=features, **tree_params).fit() diff --git a/doubleml/irm/lpq.py b/doubleml/irm/lpq.py index 1285d4eb3..e9c219935 100644 --- a/doubleml/irm/lpq.py +++ b/doubleml/irm/lpq.py @@ -113,10 +113,7 @@ def __init__( trimming_threshold=1e-2, draw_sample_splitting=True, ): - super().__init__(obj_dml_data, - n_folds, - n_rep, score, - draw_sample_splitting) + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._quantile = quantile self._treatment = treatment @@ -385,10 +382,9 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa # preliminary propensity for z ml_m_z_prelim = clone(fitted_models["ml_m_z"][i_fold]) - m_z_hat_prelim = _dml_cv_predict(ml_m_z_prelim, x_train_1, z_train_1, - method="predict_proba", smpls=smpls_prelim)[ - "preds" - ] + m_z_hat_prelim = _dml_cv_predict( + ml_m_z_prelim, x_train_1, z_train_1, method="predict_proba", smpls=smpls_prelim + )["preds"] m_z_hat_prelim = _trimm(m_z_hat_prelim, self.trimming_rule, self.trimming_threshold) if self._normalize_ipw: @@ -514,7 +510,8 @@ def ipw_score(theta): # this could be adjusted to be compatible with dml1 # estimate final nuisance parameter comp_prob_hat = np.mean( - m_d_z1_hat["preds"] - m_d_z0_hat["preds"] + m_d_z1_hat["preds"] + - m_d_z0_hat["preds"] + z / m_z_hat_adj * (d - m_d_z1_hat["preds"]) - (1 - z) / (1 - m_z_hat_adj) * (d - m_d_z0_hat["preds"]) ) diff --git a/doubleml/irm/pq.py b/doubleml/irm/pq.py index a5459349e..7e7430f14 100644 --- a/doubleml/irm/pq.py +++ b/doubleml/irm/pq.py @@ -104,25 +104,23 @@ class DoubleMLPQ(NonLinearScoreMixin, DoubleML): d 0.553878 0.149858 3.696011 0.000219 0.260161 0.847595 """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m, - treatment=1, - quantile=0.5, - n_folds=5, - n_rep=1, - score='PQ', - normalize_ipw=True, - kde=None, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + def __init__( + self, + obj_dml_data, + ml_g, + ml_m, + treatment=1, + quantile=0.5, + n_folds=5, + n_rep=1, + score="PQ", + normalize_ipw=True, + kde=None, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._quantile = quantile self._treatment = treatment @@ -130,8 +128,7 @@ def __init__(self, self._kde = _default_kde else: if not callable(kde): - raise TypeError("kde should be either a callable or None. " - "%r was passed." % kde) + raise TypeError("kde should be either a callable or None. " "%r was passed." % kde) self._kde = kde self._normalize_ipw = normalize_ipw @@ -374,14 +371,14 @@ def ipw_score(theta): m_hat["models"] = fitted_models["ml_m"] # clip propensities and normalize ipw weights - m_hat['preds'] = _trimm(m_hat['preds'], self.trimming_rule, self.trimming_threshold) + m_hat["preds"] = _trimm(m_hat["preds"], self.trimming_rule, self.trimming_threshold) # this is not done in the score to save computation due to multiple score evaluations # to be able to evaluate the raw models the m_hat['preds'] are not changed if self._normalize_ipw: - m_hat_adj = _normalize_ipw(m_hat['preds'], d) + m_hat_adj = _normalize_ipw(m_hat["preds"], d) else: - m_hat_adj = m_hat['preds'] + m_hat_adj = m_hat["preds"] if self.treatment == 0: m_hat_adj = 1 - m_hat_adj diff --git a/doubleml/irm/qte.py b/doubleml/irm/qte.py index 43931a5ff..616be97c2 100644 --- a/doubleml/irm/qte.py +++ b/doubleml/irm/qte.py @@ -86,22 +86,25 @@ class DoubleMLQTE: 0.50 0.449150 0.192539 2.332782 0.019660 0.071782 0.826519 0.75 0.709606 0.193308 3.670867 0.000242 0.330731 1.088482 """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m=None, - quantiles=0.5, - n_folds=5, - n_rep=1, - score='PQ', - normalize_ipw=True, - kde=None, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): + + def __init__( + self, + obj_dml_data, + ml_g, + ml_m=None, + quantiles=0.5, + n_folds=5, + n_rep=1, + score="PQ", + normalize_ipw=True, + kde=None, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): self._dml_data = obj_dml_data - self._quantiles = np.asarray(quantiles).reshape((-1, )) + self._quantiles = np.asarray(quantiles).reshape((-1,)) self._check_quantile() self._n_quantiles = len(self._quantiles) @@ -109,8 +112,7 @@ def __init__(self, self._kde = _default_kde else: if not callable(kde): - raise TypeError('kde should be either a callable or None. ' - '%r was passed.' % kde) + raise TypeError("kde should be either a callable or None. " "%r was passed." % kde) self._kde = kde self._normalize_ipw = normalize_ipw @@ -119,7 +121,7 @@ def __init__(self, # check score self._score = score - valid_scores = ['PQ', 'LPQ', 'CVaR'] + valid_scores = ["PQ", "LPQ", "CVaR"] _check_score(self.score, valid_scores, allow_callable=False) # check data @@ -137,11 +139,12 @@ def __init__(self, _check_trimming(self._trimming_rule, self._trimming_threshold) if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) - self._learner = {'ml_g': clone(ml_g), 'ml_m': clone(ml_m)} - self._predict_method = {'ml_g': 'predict_proba', 'ml_m': 'predict_proba'} + self._learner = {"ml_g": clone(ml_g), "ml_m": clone(ml_m)} + self._predict_method = {"ml_g": "predict_proba", "ml_m": "predict_proba"} # perform sample splitting self._smpls = None @@ -152,10 +155,9 @@ def __init__(self, def __str__(self): class_name = self.__class__.__name__ - header = f'================== {class_name} Object ==================\n' + header = f"================== {class_name} Object ==================\n" fit_summary = str(self.summary) - res = header + \ - '\n------------------ Fit summary ------------------\n' + fit_summary + res = header + "\n------------------ Fit summary ------------------\n" + fit_summary return res @property @@ -200,8 +202,10 @@ def smpls(self): The partition used for cross-fitting. """ if self._smpls is None: - err_msg = ('Sample splitting not specified. Either draw samples via .draw_sample splitting() ' + - 'or set external samples via .set_sample_splitting().') + err_msg = ( + "Sample splitting not specified. Either draw samples via .draw_sample splitting() " + + "or set external samples via .set_sample_splitting()." + ) raise ValueError(err_msg) return self._smpls @@ -354,12 +358,11 @@ def summary(self): A summary for the estimated causal effect after calling :meth:`fit`. """ if self.framework is None: - col_names = ['coef', 'std err', 't', 'P>|t|'] + col_names = ["coef", "std err", "t", "P>|t|"] df_summary = pd.DataFrame(columns=col_names) else: ci = self.confint() - df_summary = generate_summary(self.coef, self.se, self.t_stat, - self.pval, ci, self.quantiles) + df_summary = generate_summary(self.coef, self.se, self.t_stat, self.pval, ci, self.quantiles) return df_summary def fit(self, n_jobs_models=None, n_jobs_cv=None, store_predictions=True, store_models=False, external_predictions=None): @@ -395,9 +398,11 @@ def fit(self, n_jobs_models=None, n_jobs_cv=None, store_predictions=True, store_ raise NotImplementedError(f"External predictions not implemented for {self.__class__.__name__}.") # parallel estimation of the quantiles - parallel = Parallel(n_jobs=n_jobs_models, verbose=0, pre_dispatch='2*n_jobs') - fitted_models = parallel(delayed(self._fit_quantile)(i_quant, n_jobs_cv, store_predictions, store_models) - for i_quant in range(self.n_quantiles)) + parallel = Parallel(n_jobs=n_jobs_models, verbose=0, pre_dispatch="2*n_jobs") + fitted_models = parallel( + delayed(self._fit_quantile)(i_quant, n_jobs_cv, store_predictions, store_models) + for i_quant in range(self.n_quantiles) + ) # combine the estimates and scores framework_list = [None] * self.n_quantiles @@ -408,15 +413,14 @@ def fit(self, n_jobs_models=None, n_jobs_cv=None, store_predictions=True, store_ self._modellist_1[i_quant] = fitted_models[i_quant][1] # set up the framework - framework_list[i_quant] = self._modellist_1[i_quant].framework - \ - self._modellist_0[i_quant].framework + framework_list[i_quant] = self._modellist_1[i_quant].framework - self._modellist_0[i_quant].framework # aggregate all frameworks self._framework = concat(framework_list) return self - def bootstrap(self, method='normal', n_rep_boot=500): + def bootstrap(self, method="normal", n_rep_boot=500): """ Multiplier bootstrap for DoubleML models. @@ -434,7 +438,7 @@ def bootstrap(self, method='normal', n_rep_boot=500): self : object """ if self._framework is None: - raise ValueError('Apply fit() before bootstrap().') + raise ValueError("Apply fit() before bootstrap().") self._framework.bootstrap(method=method, n_rep_boot=n_rep_boot) return self @@ -450,10 +454,9 @@ def draw_sample_splitting(self): ------- self : object """ - obj_dml_resampling = DoubleMLResampling(n_folds=self.n_folds, - n_rep=self.n_rep, - n_obs=self._dml_data.n_obs, - stratify=self._dml_data.d) + obj_dml_resampling = DoubleMLResampling( + n_folds=self.n_folds, n_rep=self.n_rep, n_obs=self._dml_data.n_obs, stratify=self._dml_data.d + ) self._smpls = obj_dml_resampling.split_samples() # initialize all models self._modellist_0, self._modellist_1 = self._initialize_models() @@ -519,7 +522,8 @@ def set_sample_splitting(self, all_smpls, all_smpls_cluster=None): >>> dml_plr_obj.set_sample_splitting(smpls) """ self._smpls, self._smpls_cluster, self._n_rep, self._n_folds = _check_sample_splitting( - all_smpls, all_smpls_cluster, self._dml_data, self._is_cluster_data) + all_smpls, all_smpls_cluster, self._dml_data, self._is_cluster_data + ) # initialize all models self._modellist_0, self._modellist_1 = self._initialize_models() @@ -547,14 +551,14 @@ def confint(self, joint=False, level=0.95): """ if self.framework is None: - raise ValueError('Apply fit() before confint().') + raise ValueError("Apply fit() before confint().") df_ci = self.framework.confint(joint=joint, level=level) df_ci.set_index(pd.Index(self._quantiles), inplace=True) return df_ci - def p_adjust(self, method='romano-wolf'): + def p_adjust(self, method="romano-wolf"): """ Multiple testing adjustment for DoubleML models. @@ -573,7 +577,7 @@ def p_adjust(self, method='romano-wolf'): """ if self.framework is None: - raise ValueError('Apply fit() before p_adjust().') + raise ValueError("Apply fit() before p_adjust().") p_val, _ = self.framework.p_adjust(method=method) p_val.set_index(pd.Index(self._quantiles), inplace=True) @@ -592,59 +596,43 @@ def _fit_quantile(self, i_quant, n_jobs_cv=None, store_predictions=True, store_m def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) _check_zero_one_treatment(self) return def _check_quantile(self): if np.any(self.quantiles <= 0) | np.any(self.quantiles >= 1): - raise ValueError('Quantiles have be between 0 or 1. ' + - f'Quantiles {str(self.quantiles)} passed.') + raise ValueError("Quantiles have be between 0 or 1. " + f"Quantiles {str(self.quantiles)} passed.") def _initialize_models(self): modellist_0 = [None] * self.n_quantiles modellist_1 = [None] * self.n_quantiles kwargs = { - 'obj_dml_data': self._dml_data, - 'ml_g': self._learner['ml_g'], - 'ml_m': self._learner['ml_m'], - 'n_folds': self.n_folds, - 'n_rep': self.n_rep, - 'trimming_rule': self.trimming_rule, - 'trimming_threshold': self.trimming_threshold, - 'normalize_ipw': self.normalize_ipw, - 'draw_sample_splitting': False + "obj_dml_data": self._dml_data, + "ml_g": self._learner["ml_g"], + "ml_m": self._learner["ml_m"], + "n_folds": self.n_folds, + "n_rep": self.n_rep, + "trimming_rule": self.trimming_rule, + "trimming_threshold": self.trimming_threshold, + "normalize_ipw": self.normalize_ipw, + "draw_sample_splitting": False, } for i_quant in range(self.n_quantiles): # initialize models for both potential quantiles - if self.score == 'PQ': - model_0 = DoubleMLPQ(quantile=self._quantiles[i_quant], - treatment=0, - kde=self.kde, - **kwargs) - model_1 = DoubleMLPQ(quantile=self._quantiles[i_quant], - treatment=1, - kde=self.kde, - **kwargs) - elif self.score == 'LPQ': - model_0 = DoubleMLLPQ(quantile=self._quantiles[i_quant], - treatment=0, - kde=self.kde, - **kwargs) - model_1 = DoubleMLLPQ(quantile=self._quantiles[i_quant], - treatment=1, - kde=self.kde, - **kwargs) - - elif self.score == 'CVaR': - model_0 = DoubleMLCVAR(quantile=self._quantiles[i_quant], - treatment=0, - **kwargs) - model_1 = DoubleMLCVAR(quantile=self._quantiles[i_quant], - treatment=1, - **kwargs) + if self.score == "PQ": + model_0 = DoubleMLPQ(quantile=self._quantiles[i_quant], treatment=0, kde=self.kde, **kwargs) + model_1 = DoubleMLPQ(quantile=self._quantiles[i_quant], treatment=1, kde=self.kde, **kwargs) + elif self.score == "LPQ": + model_0 = DoubleMLLPQ(quantile=self._quantiles[i_quant], treatment=0, kde=self.kde, **kwargs) + model_1 = DoubleMLLPQ(quantile=self._quantiles[i_quant], treatment=1, kde=self.kde, **kwargs) + + elif self.score == "CVaR": + model_0 = DoubleMLCVAR(quantile=self._quantiles[i_quant], treatment=0, **kwargs) + model_1 = DoubleMLCVAR(quantile=self._quantiles[i_quant], treatment=1, **kwargs) # synchronize the sample splitting model_0.set_sample_splitting(all_smpls=self.smpls) diff --git a/doubleml/irm/ssm.py b/doubleml/irm/ssm.py index 5b2362699..1d0965a9f 100644 --- a/doubleml/irm/ssm.py +++ b/doubleml/irm/ssm.py @@ -98,23 +98,21 @@ class DoubleMLSSM(LinearScoreMixin, DoubleML): Potential outcomes Y(0) and Y(1) are estimated and ATE is returned as E[Y(1) - Y(0)]. """ - def __init__(self, - obj_dml_data, - ml_g, - ml_pi, - ml_m, - n_folds=5, - n_rep=1, - score='missing-at-random', - normalize_ipw=False, - trimming_rule='truncate', - trimming_threshold=1e-2, - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + def __init__( + self, + obj_dml_data, + ml_g, + ml_pi, + ml_m, + n_folds=5, + n_rep=1, + score="missing-at-random", + normalize_ipw=False, + trimming_rule="truncate", + trimming_threshold=1e-2, + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._external_predictions_implemented = False self._sensitivity_implemented = False @@ -125,37 +123,38 @@ def __init__(self, _check_trimming(self._trimming_rule, self._trimming_threshold) self._check_data(self._dml_data) - _check_score(self.score, ['missing-at-random', 'nonignorable']) + _check_score(self.score, ["missing-at-random", "nonignorable"]) # for both score function stratification by d and s is viable self._strata = self._dml_data.d.reshape(-1, 1) + 2 * self._dml_data.s.reshape(-1, 1) if draw_sample_splitting: self.draw_sample_splitting() - ml_g_is_classifier = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - _ = self._check_learner(ml_pi, 'ml_pi', regressor=False, classifier=True) - _ = self._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - - self._learner = {'ml_g': clone(ml_g), - 'ml_pi': clone(ml_pi), - 'ml_m': clone(ml_m), - } - self._predict_method = {'ml_g': 'predict', - 'ml_pi': 'predict_proba', - 'ml_m': 'predict_proba' - } + ml_g_is_classifier = self._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + _ = self._check_learner(ml_pi, "ml_pi", regressor=False, classifier=True) + _ = self._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + + self._learner = { + "ml_g": clone(ml_g), + "ml_pi": clone(ml_pi), + "ml_m": clone(ml_m), + } + self._predict_method = {"ml_g": "predict", "ml_pi": "predict_proba", "ml_m": "predict_proba"} if ml_g_is_classifier: if self._dml_data._check_binary_outcome(): - self._predict_method['ml_g'] = 'predict_proba' + self._predict_method["ml_g"] = "predict_proba" else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome is not binary with values 0 and 1." + ) self._initialize_ml_nuisance_params() if not isinstance(self.normalize_ipw, bool): - raise TypeError('Normalization indicator has to be boolean. ' + - f'Object of type {str(type(self.normalize_ipw))} passed.') + raise TypeError( + "Normalization indicator has to be boolean. " + f"Object of type {str(type(self.normalize_ipw))} passed." + ) @property def normalize_ipw(self): @@ -179,24 +178,26 @@ def trimming_threshold(self): return self._trimming_threshold def _initialize_ml_nuisance_params(self): - valid_learner = ['ml_g_d0', 'ml_g_d1', - 'ml_pi', 'ml_m'] - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in - valid_learner} + valid_learner = ["ml_g_d0", "ml_g_d1", "ml_pi", "ml_m"] + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') - if obj_dml_data.z_cols is not None and self._score == 'missing-at-random': - warnings.warn(' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ' - 'You are estimating the effect under the assumption of data missing at random. \ - Instrumental variables will not be used in estimation.') - if obj_dml_data.z_cols is None and self._score == 'nonignorable': - raise ValueError('Sample selection by nonignorable nonresponse was set but instrumental variable \ + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) + if obj_dml_data.z_cols is not None and self._score == "missing-at-random": + warnings.warn( + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + "You are estimating the effect under the assumption of data missing at random. \ + Instrumental variables will not be used in estimation." + ) + if obj_dml_data.z_cols is None and self._score == "nonignorable": + raise ValueError( + "Sample selection by nonignorable nonresponse was set but instrumental variable \ is None. To estimate treatment effect under nonignorable nonresponse, \ - specify an instrument for the selection variable.') + specify an instrument for the selection variable." + ) return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): @@ -204,7 +205,7 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) x, s = check_X_y(x, self._dml_data.s, force_all_finite=False) - if self._score == 'nonignorable': + if self._score == "nonignorable": z, _ = check_X_y(self._dml_data.z, y, force_all_finite=False) dx = np.column_stack((x, d, z)) else: @@ -212,49 +213,75 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa _, smpls_d0_s1, _, smpls_d1_s1 = _get_cond_smpls_2d(smpls, d, s) - if self._score == 'missing-at-random': - pi_hat = _dml_cv_predict(self._learner['ml_pi'], dx, s, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_pi'), method=self._predict_method['ml_pi'], - return_models=return_models) - pi_hat['targets'] = pi_hat['targets'].astype(float) - _check_finite_predictions(pi_hat['preds'], self._learner['ml_pi'], 'ml_pi', smpls) + if self._score == "missing-at-random": + pi_hat = _dml_cv_predict( + self._learner["ml_pi"], + dx, + s, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_pi"), + method=self._predict_method["ml_pi"], + return_models=return_models, + ) + pi_hat["targets"] = pi_hat["targets"].astype(float) + _check_finite_predictions(pi_hat["preds"], self._learner["ml_pi"], "ml_pi", smpls) # propensity score m - m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - m_hat['targets'] = m_hat['targets'].astype(float) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + m_hat["targets"] = m_hat["targets"].astype(float) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) # conditional outcome - g_hat_d1 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d1_s1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g_d1'), method=self._predict_method['ml_g'], - return_models=return_models) - g_hat_d1['targets'] = g_hat_d1['targets'].astype(float) - _check_finite_predictions(g_hat_d1['preds'], self._learner['ml_g'], 'ml_g_d1', smpls) - - g_hat_d0 = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls_d0_s1, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g_d0'), method=self._predict_method['ml_g'], - return_models=return_models) - g_hat_d0['targets'] = g_hat_d0['targets'].astype(float) - _check_finite_predictions(g_hat_d0['preds'], self._learner['ml_g'], 'ml_g_d0', smpls) + g_hat_d1 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d1_s1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g_d1"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + g_hat_d1["targets"] = g_hat_d1["targets"].astype(float) + _check_finite_predictions(g_hat_d1["preds"], self._learner["ml_g"], "ml_g_d1", smpls) + + g_hat_d0 = _dml_cv_predict( + self._learner["ml_g"], + x, + y, + smpls=smpls_d0_s1, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g_d0"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + g_hat_d0["targets"] = g_hat_d0["targets"].astype(float) + _check_finite_predictions(g_hat_d0["preds"], self._learner["ml_g"], "ml_g_d0", smpls) else: - assert self._score == 'nonignorable' + assert self._score == "nonignorable" # initialize nuisance predictions, targets and models - g_hat_d1 = {'models': None, - 'targets': np.full(shape=self._dml_data.n_obs, fill_value=np.nan), - 'preds': np.full(shape=self._dml_data.n_obs, fill_value=np.nan) - } + g_hat_d1 = { + "models": None, + "targets": np.full(shape=self._dml_data.n_obs, fill_value=np.nan), + "preds": np.full(shape=self._dml_data.n_obs, fill_value=np.nan), + } g_hat_d0 = copy.deepcopy(g_hat_d1) pi_hat = copy.deepcopy(g_hat_d1) m_hat = copy.deepcopy(g_hat_d1) # pi_hat - used for preliminary estimation of propensity score pi, overwritten in each iteration - pi_hat_prelim = {'models': None, - 'targets': [], - 'preds': [] - } + pi_hat_prelim = {"models": None, "targets": [], "preds": []} # initialize models fitted_models = {} @@ -262,8 +289,8 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa # set nuisance model parameters est_params = self._get_params(learner) - if learner == 'ml_g_d1' or learner == 'ml_g_d0': - nuisance = 'ml_g' + if learner == "ml_g_d1" or learner == "ml_g_d0": + nuisance = "ml_g" else: nuisance = learner @@ -291,90 +318,91 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa dx_train_1 = dx[train_inds_1, :] # fit propensity score for selection on first part of training set - fitted_models['ml_pi'][i_fold].fit(dx_train_1, s_train_1) - pi_hat_prelim['preds'] = _predict_zero_one_propensity(fitted_models['ml_pi'][i_fold], dx) - pi_hat_prelim['targets'] = s + fitted_models["ml_pi"][i_fold].fit(dx_train_1, s_train_1) + pi_hat_prelim["preds"] = _predict_zero_one_propensity(fitted_models["ml_pi"][i_fold], dx) + pi_hat_prelim["targets"] = s # predictions for small pi in denominator - pi_hat['preds'][test_inds] = pi_hat_prelim['preds'][test_inds] - pi_hat['targets'][test_inds] = s[test_inds] + pi_hat["preds"][test_inds] = pi_hat_prelim["preds"][test_inds] + pi_hat["targets"][test_inds] = s[test_inds] # add predicted selection propensity to covariates - xpi = np.column_stack((x, pi_hat_prelim['preds'])) + xpi = np.column_stack((x, pi_hat_prelim["preds"])) # estimate propensity score m using the second training sample xpi_train_2 = xpi[train_inds_2, :] d_train_2 = d[train_inds_2] xpi_test = xpi[test_inds, :] - fitted_models['ml_m'][i_fold].fit(xpi_train_2, d_train_2) + fitted_models["ml_m"][i_fold].fit(xpi_train_2, d_train_2) - m_hat['preds'][test_inds] = _predict_zero_one_propensity(fitted_models['ml_m'][i_fold], xpi_test) - m_hat['targets'][test_inds] = d[test_inds] + m_hat["preds"][test_inds] = _predict_zero_one_propensity(fitted_models["ml_m"][i_fold], xpi_test) + m_hat["targets"][test_inds] = d[test_inds] # estimate conditional outcome g on second training sample - treatment - s1_d1_train_2_indices = np.intersect1d(np.where(d == 1)[0], - np.intersect1d(np.where(s == 1)[0], train_inds_2)) + s1_d1_train_2_indices = np.intersect1d(np.where(d == 1)[0], np.intersect1d(np.where(s == 1)[0], train_inds_2)) xpi_s1_d1_train_2 = xpi[s1_d1_train_2_indices, :] y_s1_d1_train_2 = y[s1_d1_train_2_indices] - fitted_models['ml_g_d1'][i_fold].fit(xpi_s1_d1_train_2, y_s1_d1_train_2) + fitted_models["ml_g_d1"][i_fold].fit(xpi_s1_d1_train_2, y_s1_d1_train_2) # predict conditional outcome - g_hat_d1['preds'][test_inds] = fitted_models['ml_g_d1'][i_fold].predict(xpi_test) - g_hat_d1['targets'][test_inds] = y[test_inds] + g_hat_d1["preds"][test_inds] = fitted_models["ml_g_d1"][i_fold].predict(xpi_test) + g_hat_d1["targets"][test_inds] = y[test_inds] # estimate conditional outcome on second training sample - control - s1_d0_train_2_indices = np.intersect1d(np.where(d == 0)[0], - np.intersect1d(np.where(s == 1)[0], train_inds_2)) + s1_d0_train_2_indices = np.intersect1d(np.where(d == 0)[0], np.intersect1d(np.where(s == 1)[0], train_inds_2)) xpi_s1_d0_train_2 = xpi[s1_d0_train_2_indices, :] y_s1_d0_train_2 = y[s1_d0_train_2_indices] - fitted_models['ml_g_d0'][i_fold].fit(xpi_s1_d0_train_2, y_s1_d0_train_2) + fitted_models["ml_g_d0"][i_fold].fit(xpi_s1_d0_train_2, y_s1_d0_train_2) # predict conditional outcome - g_hat_d0['preds'][test_inds] = fitted_models['ml_g_d0'][i_fold].predict(xpi_test) - g_hat_d0['targets'][test_inds] = y[test_inds] + g_hat_d0["preds"][test_inds] = fitted_models["ml_g_d0"][i_fold].predict(xpi_test) + g_hat_d0["targets"][test_inds] = y[test_inds] if return_models: - g_hat_d1['models'] = fitted_models['ml_g_d1'] - g_hat_d0['models'] = fitted_models['ml_g_d0'] - pi_hat['models'] = fitted_models['ml_pi'] - m_hat['models'] = fitted_models['ml_m'] + g_hat_d1["models"] = fitted_models["ml_g_d1"] + g_hat_d0["models"] = fitted_models["ml_g_d0"] + pi_hat["models"] = fitted_models["ml_pi"] + m_hat["models"] = fitted_models["ml_m"] - m_hat['preds'] = _trimm(m_hat['preds'], self._trimming_rule, self._trimming_threshold) + m_hat["preds"] = _trimm(m_hat["preds"], self._trimming_rule, self._trimming_threshold) # treatment indicator - dtreat = (d == 1) - dcontrol = (d == 0) - - psi_a, psi_b = self._score_elements(dtreat, dcontrol, g_hat_d1['preds'], - g_hat_d0['preds'], - pi_hat['preds'], - m_hat['preds'], - s, y) - - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - - preds = {'predictions': {'ml_g_d0': g_hat_d0['preds'], - 'ml_g_d1': g_hat_d1['preds'], - 'ml_pi': pi_hat['preds'], - 'ml_m': m_hat['preds']}, - 'targets': {'ml_g_d0': g_hat_d0['targets'], - 'ml_g_d1': g_hat_d1['targets'], - 'ml_pi': pi_hat['targets'], - 'ml_m': m_hat['targets']}, - 'models': {'ml_g_d0': g_hat_d0['models'], - 'ml_g_d1': g_hat_d1['models'], - 'ml_pi': pi_hat['models'], - 'ml_m': m_hat['models']} - } + dtreat = d == 1 + dcontrol = d == 0 + + psi_a, psi_b = self._score_elements( + dtreat, dcontrol, g_hat_d1["preds"], g_hat_d0["preds"], pi_hat["preds"], m_hat["preds"], s, y + ) + + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + + preds = { + "predictions": { + "ml_g_d0": g_hat_d0["preds"], + "ml_g_d1": g_hat_d1["preds"], + "ml_pi": pi_hat["preds"], + "ml_m": m_hat["preds"], + }, + "targets": { + "ml_g_d0": g_hat_d0["targets"], + "ml_g_d1": g_hat_d1["targets"], + "ml_pi": pi_hat["targets"], + "ml_m": m_hat["targets"], + }, + "models": { + "ml_g_d0": g_hat_d0["models"], + "ml_g_d1": g_hat_d1["models"], + "ml_pi": pi_hat["models"], + "ml_m": m_hat["models"], + }, + } return psi_elements, preds - def _score_elements(self, dtreat, dcontrol, g_d1, g_d0, - pi, m, s, y): + def _score_elements(self, dtreat, dcontrol, g_d1, g_d0, pi, m, s, y): # psi_a psi_a = -1 @@ -394,25 +422,22 @@ def _score_elements(self, dtreat, dcontrol, g_d1, g_d0, return psi_a, psi_b - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # time indicator is used for selection (selection not available in DoubleMLData yet) x, s = check_X_y(x, self._dml_data.s, force_all_finite=False) - if self._score == 'nonignorable': + if self._score == "nonignorable": z, _ = check_X_y(self._dml_data.z, y, force_all_finite=False) dx = np.column_stack((x, d, z)) else: dx = np.column_stack((x, d)) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_pi': None, - 'ml_m': None} + scoring_methods = {"ml_g": None, "ml_pi": None, "ml_m": None} # nuisance training sets conditional on d _, smpls_d0_s1, _, smpls_d1_s1 = _get_cond_smpls_2d(smpls, d, s) @@ -421,36 +446,65 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ train_inds_d1_s1 = [train_index for (train_index, _) in smpls_d1_s1] # hyperparameter tuning for ML - g_d0_tune_res = _dml_tune(y, x, train_inds_d0_s1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - g_d1_tune_res = _dml_tune(y, x, train_inds_d1_s1, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - pi_tune_res = _dml_tune(s, dx, train_inds, - self._learner['ml_pi'], param_grids['ml_pi'], scoring_methods['ml_pi'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - m_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g_d0_tune_res = _dml_tune( + y, + x, + train_inds_d0_s1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + g_d1_tune_res = _dml_tune( + y, + x, + train_inds_d1_s1, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + pi_tune_res = _dml_tune( + s, + dx, + train_inds, + self._learner["ml_pi"], + param_grids["ml_pi"], + scoring_methods["ml_pi"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + m_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g_d0_best_params = [xx.best_params_ for xx in g_d0_tune_res] g_d1_best_params = [xx.best_params_ for xx in g_d1_tune_res] pi_best_params = [xx.best_params_ for xx in pi_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g_d0': g_d0_best_params, - 'ml_g_d1': g_d1_best_params, - 'ml_pi': pi_best_params, - 'ml_m': m_best_params} + params = {"ml_g_d0": g_d0_best_params, "ml_g_d1": g_d1_best_params, "ml_pi": pi_best_params, "ml_m": m_best_params} - tune_res = {'g_d0_tune': g_d0_tune_res, - 'g_d1_tune': g_d1_tune_res, - 'pi_tune': pi_tune_res, - 'm_tune': m_tune_res} + tune_res = {"g_d0_tune": g_d0_tune_res, "g_d1_tune": g_d1_tune_res, "pi_tune": pi_tune_res, "m_tune": m_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res diff --git a/doubleml/irm/tests/_utils_apo_manual.py b/doubleml/irm/tests/_utils_apo_manual.py index fe911b319..5852d0a91 100644 --- a/doubleml/irm/tests/_utils_apo_manual.py +++ b/doubleml/irm/tests/_utils_apo_manual.py @@ -7,12 +7,24 @@ from ...utils._estimation import _normalize_ipw -def fit_apo(y, x, d, - learner_g, learner_m, treatment_level, all_smpls, score, - n_rep=1, g0_params=None, g1_params=None, m_params=None, - normalize_ipw=False, trimming_threshold=1e-2): +def fit_apo( + y, + x, + d, + learner_g, + learner_m, + treatment_level, + all_smpls, + score, + n_rep=1, + g0_params=None, + g1_params=None, + m_params=None, + normalize_ipw=False, + trimming_threshold=1e-2, +): n_obs = len(y) - treated = (d == treatment_level) + treated = d == treatment_level thetas = np.zeros(n_rep) ses = np.zeros(n_rep) @@ -22,65 +34,84 @@ def fit_apo(y, x, d, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat0, g_hat1, m_hat = fit_nuisance_apo(y, x, d, treated, - learner_g, learner_m, smpls, score, - g0_params=g0_params, g1_params=g1_params, m_params=m_params, - trimming_threshold=trimming_threshold) + g_hat0, g_hat1, m_hat = fit_nuisance_apo( + y, + x, + d, + treated, + learner_g, + learner_m, + smpls, + score, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + trimming_threshold=trimming_threshold, + ) all_g_hat0.append(g_hat0) all_g_hat1.append(g_hat1) all_m_hat.append(m_hat) - thetas[i_rep], ses[i_rep] = apo_dml2(y, x, d, treated, - g_hat0, g_hat1, m_hat, - smpls, score, normalize_ipw) + thetas[i_rep], ses[i_rep] = apo_dml2(y, x, d, treated, g_hat0, g_hat1, m_hat, smpls, score, normalize_ipw) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_g_hat0': all_g_hat0, 'all_g_hat1': all_g_hat1, 'all_m_hat': all_m_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_g_hat0": all_g_hat0, + "all_g_hat1": all_g_hat1, + "all_m_hat": all_m_hat, + } return res -def fit_nuisance_apo(y, x, d, treated, - learner_g, learner_m, smpls, score, - g0_params=None, g1_params=None, m_params=None, - trimming_threshold=1e-12): +def fit_nuisance_apo( + y, + x, + d, + treated, + learner_g, + learner_m, + smpls, + score, + g0_params=None, + g1_params=None, + m_params=None, + trimming_threshold=1e-12, +): ml_g0 = clone(learner_g) ml_g1 = clone(learner_g) train_cond0 = np.where(treated == 0)[0] if is_classifier(learner_g): - g_hat0_list = fit_predict_proba(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict_proba(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) else: - g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) train_cond1 = np.where(treated == 1)[0] if is_classifier(learner_g): - g_hat1_list = fit_predict_proba(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) + g_hat1_list = fit_predict_proba(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) else: - g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) + g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) ml_m = clone(learner_m) - m_hat_list = fit_predict_proba(treated, x, ml_m, m_params, smpls, - trimming_threshold=trimming_threshold) + m_hat_list = fit_predict_proba(treated, x, ml_m, m_params, smpls, trimming_threshold=trimming_threshold) return g_hat0_list, g_hat1_list, m_hat_list def compute_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, smpls): - u_hat0 = np.full_like(y, np.nan, dtype='float64') - u_hat1 = np.full_like(y, np.nan, dtype='float64') - g_hat0 = np.full_like(y, np.nan, dtype='float64') - g_hat1 = np.full_like(y, np.nan, dtype='float64') - m_hat = np.full_like(y, np.nan, dtype='float64') + u_hat0 = np.full_like(y, np.nan, dtype="float64") + u_hat1 = np.full_like(y, np.nan, dtype="float64") + g_hat0 = np.full_like(y, np.nan, dtype="float64") + g_hat1 = np.full_like(y, np.nan, dtype="float64") + m_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): u_hat0[test_index] = y[test_index] - g_hat0_list[idx] u_hat1[test_index] = y[test_index] - g_hat1_list[idx] @@ -88,28 +119,22 @@ def compute_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, smpls): g_hat1[test_index] = g_hat1_list[idx] m_hat[test_index] = m_hat_list[idx] - _check_is_propensity(m_hat, 'learner_m', 'ml_m', smpls, eps=1e-12) + _check_is_propensity(m_hat, "learner_m", "ml_m", smpls, eps=1e-12) return u_hat0, u_hat1, g_hat0, g_hat1, m_hat def apo_dml2(y, x, d, treated, g_hat0_list, g_hat1_list, m_hat_list, smpls, score, normalize_ipw): n_obs = len(y) - u_hat0, u_hat1, g_hat0, g_hat1, m_hat = compute_residuals( - y, g_hat0_list, g_hat1_list, m_hat_list, smpls - ) + u_hat0, u_hat1, g_hat0, g_hat1, m_hat = compute_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, smpls) if normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, treated) else: m_hat_adj = m_hat - theta_hat = apo_orth(g_hat0, g_hat1, m_hat_adj, - u_hat0, u_hat1, treated, score) + theta_hat = apo_orth(g_hat0, g_hat1, m_hat_adj, u_hat0, u_hat1, treated, score) - se = np.sqrt(var_apo(theta_hat, g_hat0, g_hat1, - m_hat_adj, - u_hat0, u_hat1, - treated, score, n_obs)) + se = np.sqrt(var_apo(theta_hat, g_hat0, g_hat1, m_hat_adj, u_hat0, u_hat1, treated, score, n_obs)) return theta_hat, se @@ -120,14 +145,27 @@ def apo_orth(g_hat0, g_hat1, m_hat, u_hat0, u_hat1, treated, score): def var_apo(theta, g_hat0, g_hat1, m_hat, u_hat0, u_hat1, treated, score, n_obs): - var = 1/n_obs * np.mean(np.power(g_hat1 + np.divide(np.multiply(treated, u_hat1), m_hat) - theta, 2)) + var = 1 / n_obs * np.mean(np.power(g_hat1 + np.divide(np.multiply(treated, u_hat1), m_hat) - theta, 2)) return var -def boot_apo(y, d, treatment_level, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1, normalize_ipw=True): - treated = (d == treatment_level) +def boot_apo( + y, + d, + treatment_level, + thetas, + ses, + all_g_hat0, + all_g_hat1, + all_m_hat, + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep=1, + normalize_ipw=True, +): + treated = d == treatment_level all_boot_t_stat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] @@ -135,9 +173,20 @@ def boot_apo(y, d, treatment_level, thetas, ses, all_g_hat0, all_g_hat1, all_m_h weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_apo_single_split( - thetas[i_rep], y, d, treated, - all_g_hat0[i_rep], all_g_hat1[i_rep], all_m_hat[i_rep], smpls, - score, ses[i_rep], weights, n_rep_boot, normalize_ipw) + thetas[i_rep], + y, + d, + treated, + all_g_hat0[i_rep], + all_g_hat1[i_rep], + all_m_hat[i_rep], + smpls, + score, + ses[i_rep], + weights, + n_rep_boot, + normalize_ipw, + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -145,10 +194,10 @@ def boot_apo(y, d, treatment_level, thetas, ses, all_g_hat0, all_g_hat1, all_m_h return boot_t_stat -def boot_apo_single_split(theta, y, d, treated, g_hat0_list, g_hat1_list, m_hat_list, - smpls, score, se, weights, n_rep_boot, normalize_ipw): - _, u_hat1, _, g_hat1, m_hat = compute_residuals( - y, g_hat0_list, g_hat1_list, m_hat_list, smpls) +def boot_apo_single_split( + theta, y, d, treated, g_hat0_list, g_hat1_list, m_hat_list, smpls, score, se, weights, n_rep_boot, normalize_ipw +): + _, u_hat1, _, g_hat1, m_hat = compute_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, smpls) if normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, treated) @@ -165,7 +214,7 @@ def boot_apo_single_split(theta, y, d, treated, g_hat0_list, g_hat1_list, m_hat_ def fit_sensitivity_elements_apo(y, d, treatment_level, all_coef, predictions, score, n_rep): n_treat = 1 n_obs = len(y) - treated = (d == treatment_level) + treated = d == treatment_level sigma2 = np.full(shape=(1, n_rep, n_treat), fill_value=np.nan) nu2 = np.full(shape=(1, n_rep, n_treat), fill_value=np.nan) @@ -174,14 +223,14 @@ def fit_sensitivity_elements_apo(y, d, treatment_level, all_coef, predictions, s for i_rep in range(n_rep): - m_hat = predictions['ml_m'][:, i_rep, 0] - g_hat0 = predictions['ml_g0'][:, i_rep, 0] - g_hat1 = predictions['ml_g1'][:, i_rep, 0] + m_hat = predictions["ml_m"][:, i_rep, 0] + g_hat0 = predictions["ml_g0"][:, i_rep, 0] + g_hat1 = predictions["ml_g1"][:, i_rep, 0] weights = np.ones_like(d) weights_bar = np.ones_like(d) - sigma2_score_element = np.square(y - np.multiply(treated, g_hat1) - np.multiply(1.0-treated, g_hat0)) + sigma2_score_element = np.square(y - np.multiply(treated, g_hat1) - np.multiply(1.0 - treated, g_hat0)) sigma2[0, i_rep, 0] = np.mean(sigma2_score_element) psi_sigma2[:, i_rep, 0] = sigma2_score_element - sigma2[0, i_rep, 0] @@ -193,24 +242,18 @@ def fit_sensitivity_elements_apo(y, d, treatment_level, all_coef, predictions, s nu2[0, i_rep, 0] = np.mean(nu2_score_element) psi_nu2[:, i_rep, 0] = nu2_score_element - nu2[0, i_rep, 0] - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2} + element_dict = {"sigma2": sigma2, "nu2": nu2, "psi_sigma2": psi_sigma2, "psi_nu2": psi_nu2} return element_dict -def tune_nuisance_apo(y, x, d, treatment_level, ml_g, ml_m, smpls, score, n_folds_tune, - param_grid_g, param_grid_m): +def tune_nuisance_apo(y, x, d, treatment_level, ml_g, ml_m, smpls, score, n_folds_tune, param_grid_g, param_grid_m): train_cond0 = np.where(d != treatment_level)[0] - g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond0) + g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond0) train_cond1 = np.where(d == treatment_level)[0] - g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond1) + g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond1) - treated = (d == treatment_level) + treated = d == treatment_level m_tune_res = tune_grid_search(treated, x, ml_m, smpls, param_grid_m, n_folds_tune) g0_best_params = [xx.best_params_ for xx in g0_tune_res] diff --git a/doubleml/irm/tests/_utils_apos_manual.py b/doubleml/irm/tests/_utils_apos_manual.py index 1677cab9e..efc5eea13 100644 --- a/doubleml/irm/tests/_utils_apos_manual.py +++ b/doubleml/irm/tests/_utils_apos_manual.py @@ -6,10 +6,20 @@ from ..apo import DoubleMLAPO -def fit_apos(y, x, d, - learner_g, learner_m, treatment_levels, all_smpls, score, - n_rep=1, trimming_rule='truncate', - normalize_ipw=False, trimming_threshold=1e-2): +def fit_apos( + y, + x, + d, + learner_g, + learner_m, + treatment_levels, + all_smpls, + score, + n_rep=1, + trimming_rule="truncate", + normalize_ipw=False, + trimming_threshold=1e-2, +): n_obs = len(y) n_treatments = len(treatment_levels) n_folds = len(all_smpls[0]) @@ -32,7 +42,7 @@ def fit_apos(y, x, d, trimming_rule=trimming_rule, trimming_threshold=trimming_threshold, normalize_ipw=normalize_ipw, - draw_sample_splitting=False + draw_sample_splitting=False, ) # synchronize the sample splitting @@ -51,12 +61,11 @@ def fit_apos(y, x, d, apos = np.median(all_apos, axis=1) se = np.zeros(n_treatments) for i_level in range(n_treatments): - se[i_level] = np.sqrt(np.median(np.power(all_se[i_level, :], 2) * n_obs + - np.power(all_apos[i_level, :] - all_apos[i_level], 2)) / n_obs) + se[i_level] = np.sqrt( + np.median(np.power(all_se[i_level, :], 2) * n_obs + np.power(all_apos[i_level, :] - all_apos[i_level], 2)) / n_obs + ) - res = {'apos': apos, 'se': se, - 'all_apos': all_apos, 'all_se': all_se, - 'apo_scaled_score': apo_scaled_score} + res = {"apos": apos, "se": se, "all_apos": all_apos, "all_se": all_se, "apo_scaled_score": apo_scaled_score} return res @@ -67,7 +76,8 @@ def boot_apos(scaled_scores, ses, treatment_levels, all_smpls, n_rep, bootstrap, n_obs = scaled_scores.shape[0] weights = draw_weights(bootstrap, n_rep_boot, n_obs) for i_treatment_levels in range(n_treatment_levels): - boot_t_stat[:, i_treatment_levels, i_rep] = np.matmul(weights, scaled_scores[:, i_treatment_levels, i_rep]) / \ - (n_obs * ses[i_treatment_levels, i_rep]) + boot_t_stat[:, i_treatment_levels, i_rep] = np.matmul(weights, scaled_scores[:, i_treatment_levels, i_rep]) / ( + n_obs * ses[i_treatment_levels, i_rep] + ) return boot_t_stat diff --git a/doubleml/irm/tests/_utils_cvar_manual.py b/doubleml/irm/tests/_utils_cvar_manual.py index 2f523549d..7ff2c2306 100644 --- a/doubleml/irm/tests/_utils_cvar_manual.py +++ b/doubleml/irm/tests/_utils_cvar_manual.py @@ -6,9 +6,21 @@ from ...utils._estimation import _dml_cv_predict, _get_bracket_guess, _normalize_ipw, _solve_ipw_score -def fit_cvar(y, x, d, quantile, - learner_g, learner_m, all_smpls, treatment, normalize_ipw=True, n_rep=1, - trimming_threshold=1e-2, g_params=None, m_params=None): +def fit_cvar( + y, + x, + d, + quantile, + learner_g, + learner_m, + all_smpls, + treatment, + normalize_ipw=True, + n_rep=1, + trimming_threshold=1e-2, + g_params=None, + m_params=None, +): n_obs = len(y) cvars = np.zeros(n_rep) @@ -17,25 +29,34 @@ def fit_cvar(y, x, d, quantile, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat, ipw_est = fit_nuisance_cvar(y, x, d, quantile, - learner_g, learner_m, smpls, treatment, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - g_params=g_params, m_params=m_params) + g_hat, m_hat, ipw_est = fit_nuisance_cvar( + y, + x, + d, + quantile, + learner_g, + learner_m, + smpls, + treatment, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + g_params=g_params, + m_params=m_params, + ) cvars[i_rep], ses[i_rep] = cvar_dml2(y, d, g_hat, m_hat, treatment, quantile, ipw_est) cvar = np.median(cvars) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(cvars - cvar, 2)) / n_obs) - res = {'pq': cvar, 'se': se, - 'pqs': cvars, 'ses': ses} + res = {"pq": cvar, "se": se, "pqs": cvars, "ses": ses} return res -def fit_nuisance_cvar(y, x, d, quantile, learner_g, learner_m, smpls, treatment, - normalize_ipw, trimming_threshold, g_params, m_params): +def fit_nuisance_cvar( + y, x, d, quantile, learner_g, learner_m, smpls, treatment, normalize_ipw, trimming_threshold, g_params, m_params +): n_folds = len(smpls) n_obs = len(y) coef_bounds = (y.min(), y.max()) @@ -63,26 +84,24 @@ def fit_nuisance_cvar(y, x, d, quantile, learner_g, learner_m, smpls, treatment, test_inds = smpls[i_fold][1] # start nested crossfitting - train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, - random_state=42, stratify=d[train_inds]) - smpls_prelim = [(train, test) for train, test in - StratifiedKFold(n_splits=n_folds).split(X=train_inds_1, y=d[train_inds_1])] + train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, random_state=42, stratify=d[train_inds]) + smpls_prelim = [ + (train, test) for train, test in StratifiedKFold(n_splits=n_folds).split(X=train_inds_1, y=d[train_inds_1]) + ] d_train_1 = d[train_inds_1] y_train_1 = y[train_inds_1] x_train_1 = x[train_inds_1, :] # todo change prediction method - m_hat_prelim_list = fit_predict_proba(d_train_1, x_train_1, ml_m, - params=None, - trimming_threshold=trimming_threshold, - smpls=smpls_prelim) + m_hat_prelim_list = fit_predict_proba( + d_train_1, x_train_1, ml_m, params=None, trimming_threshold=trimming_threshold, smpls=smpls_prelim + ) - m_hat_prelim = np.full_like(y_train_1, np.nan, dtype='float64') + m_hat_prelim = np.full_like(y_train_1, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls_prelim): m_hat_prelim[test_index] = m_hat_prelim_list[idx] - m_hat_prelim = _dml_cv_predict(ml_m, x_train_1, d_train_1, - method='predict_proba', smpls=smpls_prelim)['preds'] + m_hat_prelim = _dml_cv_predict(ml_m, x_train_1, d_train_1, method="predict_proba", smpls=smpls_prelim)["preds"] m_hat_prelim[m_hat_prelim < trimming_threshold] = trimming_threshold m_hat_prelim[m_hat_prelim > 1 - trimming_threshold] = 1 - trimming_threshold @@ -168,16 +187,14 @@ def cvar_var_est(coef, g_hat, m_hat, d, y, treatment, quantile, ipw_est, n_obs): return var_est -def tune_nuisance_cvar(y, x, d, ml_g, ml_m, smpls, treatment, quantile, n_folds_tune, - param_grid_g, param_grid_m): +def tune_nuisance_cvar(y, x, d, ml_g, ml_m, smpls, treatment, quantile, n_folds_tune, param_grid_g, param_grid_m): train_cond_treat = np.where(d == treatment)[0] quantile_approx = np.quantile(y[d == treatment], quantile) g_target_1 = np.ones_like(y) * quantile_approx g_target_2 = (y - quantile * quantile_approx) / (1 - quantile) g_target_approx = np.max(np.column_stack((g_target_1, g_target_2)), 1) - g_tune_res = tune_grid_search(g_target_approx, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond_treat) + g_tune_res = tune_grid_search(g_target_approx, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond_treat) m_tune_res = tune_grid_search(d, x, ml_m, smpls, param_grid_m, n_folds_tune) g_best_params = [xx.best_params_ for xx in g_tune_res] diff --git a/doubleml/irm/tests/_utils_iivm_manual.py b/doubleml/irm/tests/_utils_iivm_manual.py index f70a18903..a358d6f81 100644 --- a/doubleml/irm/tests/_utils_iivm_manual.py +++ b/doubleml/irm/tests/_utils_iivm_manual.py @@ -6,10 +6,27 @@ from ...utils._estimation import _normalize_ipw -def fit_iivm(y, x, d, z, - learner_g, learner_m, learner_r, all_smpls, score, - n_rep=1, g0_params=None, g1_params=None, m_params=None, r0_params=None, r1_params=None, - normalize_ipw=True, trimming_threshold=1e-2, always_takers=True, never_takers=True): +def fit_iivm( + y, + x, + d, + z, + learner_g, + learner_m, + learner_r, + all_smpls, + score, + n_rep=1, + g0_params=None, + g1_params=None, + m_params=None, + r0_params=None, + r1_params=None, + normalize_ipw=True, + trimming_threshold=1e-2, + always_takers=True, + never_takers=True, +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -23,10 +40,23 @@ def fit_iivm(y, x, d, z, smpls = all_smpls[i_rep] g_hat0, g_hat1, m_hat, r_hat0, r_hat1 = fit_nuisance_iivm( - y, x, d, z, - learner_g, learner_m, learner_r, smpls, - g0_params=g0_params, g1_params=g1_params, m_params=m_params, r0_params=r0_params, r1_params=r1_params, - trimming_threshold=trimming_threshold, always_takers=always_takers, never_takers=never_takers) + y, + x, + d, + z, + learner_g, + learner_m, + learner_r, + smpls, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + r0_params=r0_params, + r1_params=r1_params, + trimming_threshold=trimming_threshold, + always_takers=always_takers, + never_takers=never_takers, + ) all_g_hat0.append(g_hat0) all_g_hat1.append(g_hat1) @@ -34,90 +64,112 @@ def fit_iivm(y, x, d, z, all_r_hat0.append(r_hat0) all_r_hat1.append(r_hat1) - thetas[i_rep], ses[i_rep] = iivm_dml2(y, x, d, z, - g_hat0, g_hat1, m_hat, r_hat0, r_hat1, - smpls, score, normalize_ipw) + thetas[i_rep], ses[i_rep] = iivm_dml2(y, x, d, z, g_hat0, g_hat1, m_hat, r_hat0, r_hat1, smpls, score, normalize_ipw) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_g_hat0': all_g_hat0, 'all_g_hat1': all_g_hat1, - 'all_m_hat': all_m_hat, 'all_r_hat0': all_r_hat0, 'all_r_hat1': all_r_hat1} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_g_hat0": all_g_hat0, + "all_g_hat1": all_g_hat1, + "all_m_hat": all_m_hat, + "all_r_hat0": all_r_hat0, + "all_r_hat1": all_r_hat1, + } return res -def fit_nuisance_iivm(y, x, d, z, learner_g, learner_m, learner_r, smpls, - g0_params=None, g1_params=None, m_params=None, r0_params=None, r1_params=None, - trimming_threshold=1e-12, always_takers=True, never_takers=True): +def fit_nuisance_iivm( + y, + x, + d, + z, + learner_g, + learner_m, + learner_r, + smpls, + g0_params=None, + g1_params=None, + m_params=None, + r0_params=None, + r1_params=None, + trimming_threshold=1e-12, + always_takers=True, + never_takers=True, +): ml_g0 = clone(learner_g) train_cond0 = np.where(z == 0)[0] if is_classifier(learner_g): - g_hat0_list = fit_predict_proba(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict_proba(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) else: - g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) ml_g1 = clone(learner_g) train_cond1 = np.where(z == 1)[0] if is_classifier(learner_g): - g_hat1_list = fit_predict_proba(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) + g_hat1_list = fit_predict_proba(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) else: - g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) + g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) ml_m = clone(learner_m) - m_hat_list = fit_predict_proba(z, x, ml_m, m_params, smpls, - trimming_threshold=trimming_threshold) + m_hat_list = fit_predict_proba(z, x, ml_m, m_params, smpls, trimming_threshold=trimming_threshold) ml_r0 = clone(learner_r) if always_takers: - r_hat0_list = fit_predict_proba(d, x, ml_r0, r0_params, smpls, - train_cond=train_cond0) + r_hat0_list = fit_predict_proba(d, x, ml_r0, r0_params, smpls, train_cond=train_cond0) else: r_hat0_list = [] - for (_, test_index) in smpls: + for _, test_index in smpls: r_hat0_list.append(np.zeros_like(d[test_index])) ml_r1 = clone(learner_r) if never_takers: - r_hat1_list = fit_predict_proba(d, x, ml_r1, r1_params, smpls, - train_cond=train_cond1) + r_hat1_list = fit_predict_proba(d, x, ml_r1, r1_params, smpls, train_cond=train_cond1) else: r_hat1_list = [] - for (_, test_index) in smpls: + for _, test_index in smpls: r_hat1_list.append(np.ones_like(d[test_index])) return g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list -def tune_nuisance_iivm(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_tune, - param_grid_g, param_grid_m, param_grid_r, - always_takers=True, never_takers=True): +def tune_nuisance_iivm( + y, + x, + d, + z, + ml_g, + ml_m, + ml_r, + smpls, + n_folds_tune, + param_grid_g, + param_grid_m, + param_grid_r, + always_takers=True, + never_takers=True, +): train_cond0 = np.where(z == 0)[0] - g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond0) + g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond0) train_cond1 = np.where(z == 1)[0] - g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond1) + g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond1) m_tune_res = tune_grid_search(z, x, ml_m, smpls, param_grid_m, n_folds_tune) if always_takers: - r0_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune, - train_cond=train_cond0) + r0_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune, train_cond=train_cond0) r0_best_params = [xx.best_params_ for xx in r0_tune_res] else: r0_best_params = None if never_takers: - r1_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune, - train_cond=train_cond1) + r1_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune, train_cond=train_cond1) r1_best_params = [xx.best_params_ for xx in r1_tune_res] else: r1_best_params = None @@ -130,15 +182,15 @@ def tune_nuisance_iivm(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_tune, def compute_iivm_residuals(y, d, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, smpls): - u_hat0 = np.full_like(y, np.nan, dtype='float64') - u_hat1 = np.full_like(y, np.nan, dtype='float64') - w_hat0 = np.full_like(y, np.nan, dtype='float64') - w_hat1 = np.full_like(y, np.nan, dtype='float64') - g_hat0 = np.full_like(y, np.nan, dtype='float64') - g_hat1 = np.full_like(y, np.nan, dtype='float64') - r_hat0 = np.full_like(y, np.nan, dtype='float64') - r_hat1 = np.full_like(y, np.nan, dtype='float64') - m_hat = np.full_like(y, np.nan, dtype='float64') + u_hat0 = np.full_like(y, np.nan, dtype="float64") + u_hat1 = np.full_like(y, np.nan, dtype="float64") + w_hat0 = np.full_like(y, np.nan, dtype="float64") + w_hat1 = np.full_like(y, np.nan, dtype="float64") + g_hat0 = np.full_like(y, np.nan, dtype="float64") + g_hat1 = np.full_like(y, np.nan, dtype="float64") + r_hat0 = np.full_like(y, np.nan, dtype="float64") + r_hat1 = np.full_like(y, np.nan, dtype="float64") + m_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): u_hat0[test_index] = y[test_index] - g_hat0_list[idx] u_hat1[test_index] = y[test_index] - g_hat1_list[idx] @@ -156,53 +208,87 @@ def compute_iivm_residuals(y, d, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_li def iivm_dml2(y, x, d, z, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, smpls, score, normalize_ipw): n_obs = len(y) u_hat0, u_hat1, w_hat0, w_hat1, g_hat0, g_hat1, m_hat, r_hat0, r_hat1 = compute_iivm_residuals( - y, d, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, smpls) + y, d, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, smpls + ) if normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, d) else: m_hat_adj = m_hat - theta_hat = iivm_orth(g_hat0, g_hat1, m_hat_adj, r_hat0, r_hat1, - u_hat0, u_hat1, w_hat0, w_hat1, z, score) - se = np.sqrt(var_iivm(theta_hat, g_hat0, g_hat1, - m_hat_adj, r_hat0, r_hat1, - u_hat0, u_hat1, w_hat0, w_hat1, - z, score, n_obs)) + theta_hat = iivm_orth(g_hat0, g_hat1, m_hat_adj, r_hat0, r_hat1, u_hat0, u_hat1, w_hat0, w_hat1, z, score) + se = np.sqrt( + var_iivm(theta_hat, g_hat0, g_hat1, m_hat_adj, r_hat0, r_hat1, u_hat0, u_hat1, w_hat0, w_hat1, z, score, n_obs) + ) return theta_hat, se def var_iivm(theta, g_hat0, g_hat1, m_hat, r_hat0, r_hat1, u_hat0, u_hat1, w_hat0, w_hat1, z, score, n_obs): - assert score == 'LATE' - var = 1/n_obs * np.mean(np.power(g_hat1 - g_hat0 - + np.divide(np.multiply(z, u_hat1), m_hat) - - np.divide(np.multiply(1.-z, u_hat0), 1.-m_hat) - - theta*(r_hat1 - r_hat0 - + np.divide(np.multiply(z, w_hat1), m_hat) - - np.divide(np.multiply(1.-z, w_hat0), 1.-m_hat)), 2)) \ - / np.power(np.mean(r_hat1 - r_hat0 - + np.divide(np.multiply(z, w_hat1), m_hat) - - np.divide(np.multiply(1.-z, w_hat0), 1.-m_hat)), 2) + assert score == "LATE" + var = ( + 1 + / n_obs + * np.mean( + np.power( + g_hat1 + - g_hat0 + + np.divide(np.multiply(z, u_hat1), m_hat) + - np.divide(np.multiply(1.0 - z, u_hat0), 1.0 - m_hat) + - theta + * ( + r_hat1 + - r_hat0 + + np.divide(np.multiply(z, w_hat1), m_hat) + - np.divide(np.multiply(1.0 - z, w_hat0), 1.0 - m_hat) + ), + 2, + ) + ) + / np.power( + np.mean( + r_hat1 + - r_hat0 + + np.divide(np.multiply(z, w_hat1), m_hat) + - np.divide(np.multiply(1.0 - z, w_hat0), 1.0 - m_hat) + ), + 2, + ) + ) return var def iivm_orth(g_hat0, g_hat1, m_hat, r_hat0, r_hat1, u_hat0, u_hat1, w_hat0, w_hat1, z, score): - assert score == 'LATE' - res = np.mean(g_hat1 - g_hat0 - + np.divide(np.multiply(z, u_hat1), m_hat) - - np.divide(np.multiply(1.-z, u_hat0), 1.-m_hat)) \ - / np.mean(r_hat1 - r_hat0 - + np.divide(np.multiply(z, w_hat1), m_hat) - - np.divide(np.multiply(1.-z, w_hat0), 1.-m_hat)) + assert score == "LATE" + res = np.mean( + g_hat1 - g_hat0 + np.divide(np.multiply(z, u_hat1), m_hat) - np.divide(np.multiply(1.0 - z, u_hat0), 1.0 - m_hat) + ) / np.mean( + r_hat1 - r_hat0 + np.divide(np.multiply(z, w_hat1), m_hat) - np.divide(np.multiply(1.0 - z, w_hat0), 1.0 - m_hat) + ) return res -def boot_iivm(y, d, z, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, all_r_hat0, all_r_hat1, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1, apply_cross_fitting=True, normalize_ipw=True): +def boot_iivm( + y, + d, + z, + thetas, + ses, + all_g_hat0, + all_g_hat1, + all_m_hat, + all_r_hat0, + all_r_hat1, + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep=1, + apply_cross_fitting=True, + normalize_ipw=True, +): all_boot_t_stat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] @@ -213,9 +299,23 @@ def boot_iivm(y, d, z, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, all_r_hat n_obs = len(test_index) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_iivm_single_split( - thetas[i_rep], y, d, z, - all_g_hat0[i_rep], all_g_hat1[i_rep], all_m_hat[i_rep], all_r_hat0[i_rep], all_r_hat1[i_rep], - smpls, score, ses[i_rep], weights, n_rep_boot, apply_cross_fitting, normalize_ipw) + thetas[i_rep], + y, + d, + z, + all_g_hat0[i_rep], + all_g_hat1[i_rep], + all_m_hat[i_rep], + all_r_hat0[i_rep], + all_r_hat1[i_rep], + smpls, + score, + ses[i_rep], + weights, + n_rep_boot, + apply_cross_fitting, + normalize_ipw, + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -223,11 +323,28 @@ def boot_iivm(y, d, z, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, all_r_hat return boot_t_stat -def boot_iivm_single_split(theta, y, d, z, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, - smpls, score, se, weights, n_rep, apply_cross_fitting, normalize_ipw): - assert score == 'LATE' +def boot_iivm_single_split( + theta, + y, + d, + z, + g_hat0_list, + g_hat1_list, + m_hat_list, + r_hat0_list, + r_hat1_list, + smpls, + score, + se, + weights, + n_rep, + apply_cross_fitting, + normalize_ipw, +): + assert score == "LATE" u_hat0, u_hat1, w_hat0, w_hat1, g_hat0, g_hat1, m_hat, r_hat0, r_hat1 = compute_iivm_residuals( - y, d, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, smpls) + y, d, g_hat0_list, g_hat1_list, m_hat_list, r_hat0_list, r_hat1_list, smpls + ) if normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, d) @@ -235,22 +352,38 @@ def boot_iivm_single_split(theta, y, d, z, g_hat0_list, g_hat1_list, m_hat_list, m_hat_adj = m_hat if apply_cross_fitting: - J = np.mean(-(r_hat1 - r_hat0 - + np.divide(np.multiply(z, w_hat1), m_hat_adj) - - np.divide(np.multiply(1. - z, w_hat0), 1. - m_hat_adj))) + J = np.mean( + -( + r_hat1 + - r_hat0 + + np.divide(np.multiply(z, w_hat1), m_hat_adj) + - np.divide(np.multiply(1.0 - z, w_hat0), 1.0 - m_hat_adj) + ) + ) else: test_index = smpls[0][1] - J = np.mean(-(r_hat1[test_index] - r_hat0[test_index] - + np.divide(np.multiply(z[test_index], w_hat1[test_index]), m_hat_adj[test_index]) - - np.divide(np.multiply(1. - z[test_index], w_hat0[test_index]), - 1. - m_hat_adj[test_index]))) - - psi = g_hat1 - g_hat0 \ - + np.divide(np.multiply(z, u_hat1), m_hat_adj) \ - - np.divide(np.multiply(1.-z, u_hat0), 1.-m_hat_adj) \ - - theta*(r_hat1 - r_hat0 - + np.divide(np.multiply(z, w_hat1), m_hat_adj) - - np.divide(np.multiply(1.-z, w_hat0), 1.-m_hat_adj)) + J = np.mean( + -( + r_hat1[test_index] + - r_hat0[test_index] + + np.divide(np.multiply(z[test_index], w_hat1[test_index]), m_hat_adj[test_index]) + - np.divide(np.multiply(1.0 - z[test_index], w_hat0[test_index]), 1.0 - m_hat_adj[test_index]) + ) + ) + + psi = ( + g_hat1 + - g_hat0 + + np.divide(np.multiply(z, u_hat1), m_hat_adj) + - np.divide(np.multiply(1.0 - z, u_hat0), 1.0 - m_hat_adj) + - theta + * ( + r_hat1 + - r_hat0 + + np.divide(np.multiply(z, w_hat1), m_hat_adj) + - np.divide(np.multiply(1.0 - z, w_hat0), 1.0 - m_hat_adj) + ) + ) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep, apply_cross_fitting) diff --git a/doubleml/irm/tests/_utils_irm_manual.py b/doubleml/irm/tests/_utils_irm_manual.py index 24678db10..774ee6558 100644 --- a/doubleml/irm/tests/_utils_irm_manual.py +++ b/doubleml/irm/tests/_utils_irm_manual.py @@ -7,10 +7,21 @@ from ...utils._estimation import _normalize_ipw -def fit_irm(y, x, d, - learner_g, learner_m, all_smpls, score, - n_rep=1, g0_params=None, g1_params=None, m_params=None, - normalize_ipw=True, trimming_threshold=1e-2): +def fit_irm( + y, + x, + d, + learner_g, + learner_m, + all_smpls, + score, + n_rep=1, + g0_params=None, + g1_params=None, + m_params=None, + normalize_ipw=True, + trimming_threshold=1e-2, +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -22,55 +33,63 @@ def fit_irm(y, x, d, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat0, g_hat1, m_hat, p_hat = fit_nuisance_irm(y, x, d, - learner_g, learner_m, smpls, - score, - g0_params=g0_params, g1_params=g1_params, m_params=m_params, - trimming_threshold=trimming_threshold) + g_hat0, g_hat1, m_hat, p_hat = fit_nuisance_irm( + y, + x, + d, + learner_g, + learner_m, + smpls, + score, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + trimming_threshold=trimming_threshold, + ) all_g_hat0.append(g_hat0) all_g_hat1.append(g_hat1) all_m_hat.append(m_hat) all_p_hat.append(p_hat) - thetas[i_rep], ses[i_rep] = irm_dml2(y, x, d, - g_hat0, g_hat1, m_hat, p_hat, - smpls, score, normalize_ipw) + thetas[i_rep], ses[i_rep] = irm_dml2(y, x, d, g_hat0, g_hat1, m_hat, p_hat, smpls, score, normalize_ipw) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_g_hat0': all_g_hat0, 'all_g_hat1': all_g_hat1, 'all_m_hat': all_m_hat, 'all_p_hat': all_p_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_g_hat0": all_g_hat0, + "all_g_hat1": all_g_hat1, + "all_m_hat": all_m_hat, + "all_p_hat": all_p_hat, + } return res -def fit_nuisance_irm(y, x, d, learner_g, learner_m, smpls, score, - g0_params=None, g1_params=None, m_params=None, - trimming_threshold=1e-12): +def fit_nuisance_irm( + y, x, d, learner_g, learner_m, smpls, score, g0_params=None, g1_params=None, m_params=None, trimming_threshold=1e-12 +): ml_g0 = clone(learner_g) ml_g1 = clone(learner_g) train_cond0 = np.where(d == 0)[0] if is_classifier(learner_g): - g_hat0_list = fit_predict_proba(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict_proba(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) else: - g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, - train_cond=train_cond0) + g_hat0_list = fit_predict(y, x, ml_g0, g0_params, smpls, train_cond=train_cond0) train_cond1 = np.where(d == 1)[0] if is_classifier(learner_g): - g_hat1_list = fit_predict_proba(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) + g_hat1_list = fit_predict_proba(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) else: - g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, - train_cond=train_cond1) + g_hat1_list = fit_predict(y, x, ml_g1, g1_params, smpls, train_cond=train_cond1) ml_m = clone(learner_m) - m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls, - trimming_threshold=trimming_threshold) + m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls, trimming_threshold=trimming_threshold) p_hat_list = [] for _ in smpls: @@ -79,15 +98,12 @@ def fit_nuisance_irm(y, x, d, learner_g, learner_m, smpls, score, return g_hat0_list, g_hat1_list, m_hat_list, p_hat_list -def tune_nuisance_irm(y, x, d, ml_g, ml_m, smpls, score, n_folds_tune, - param_grid_g, param_grid_m): +def tune_nuisance_irm(y, x, d, ml_g, ml_m, smpls, score, n_folds_tune, param_grid_g, param_grid_m): train_cond0 = np.where(d == 0)[0] - g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond0) + g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond0) train_cond1 = np.where(d == 1)[0] - g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond1) + g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond1) m_tune_res = tune_grid_search(d, x, ml_m, smpls, param_grid_m, n_folds_tune) @@ -99,12 +115,12 @@ def tune_nuisance_irm(y, x, d, ml_g, ml_m, smpls, score, n_folds_tune, def compute_iivm_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls): - u_hat0 = np.full_like(y, np.nan, dtype='float64') - u_hat1 = np.full_like(y, np.nan, dtype='float64') - g_hat0 = np.full_like(y, np.nan, dtype='float64') - g_hat1 = np.full_like(y, np.nan, dtype='float64') - m_hat = np.full_like(y, np.nan, dtype='float64') - p_hat = np.full_like(y, np.nan, dtype='float64') + u_hat0 = np.full_like(y, np.nan, dtype="float64") + u_hat1 = np.full_like(y, np.nan, dtype="float64") + g_hat0 = np.full_like(y, np.nan, dtype="float64") + g_hat1 = np.full_like(y, np.nan, dtype="float64") + m_hat = np.full_like(y, np.nan, dtype="float64") + p_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): u_hat0[test_index] = y[test_index] - g_hat0_list[idx] u_hat1[test_index] = y[test_index] - g_hat1_list[idx] @@ -113,66 +129,96 @@ def compute_iivm_residuals(y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, m_hat[test_index] = m_hat_list[idx] p_hat[test_index] = p_hat_list[idx] - _check_is_propensity(m_hat, 'learner_m', 'ml_m', smpls, eps=1e-12) + _check_is_propensity(m_hat, "learner_m", "ml_m", smpls, eps=1e-12) return u_hat0, u_hat1, g_hat0, g_hat1, m_hat, p_hat def irm_dml2(y, x, d, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls, score, normalize_ipw): n_obs = len(y) u_hat0, u_hat1, g_hat0, g_hat1, m_hat, p_hat = compute_iivm_residuals( - y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls) + y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls + ) if normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, d) else: m_hat_adj = m_hat - theta_hat = irm_orth(g_hat0, g_hat1, m_hat_adj, p_hat, - u_hat0, u_hat1, d, score) - se = np.sqrt(var_irm(theta_hat, g_hat0, g_hat1, - m_hat_adj, p_hat, - u_hat0, u_hat1, - d, score, n_obs)) + theta_hat = irm_orth(g_hat0, g_hat1, m_hat_adj, p_hat, u_hat0, u_hat1, d, score) + se = np.sqrt(var_irm(theta_hat, g_hat0, g_hat1, m_hat_adj, p_hat, u_hat0, u_hat1, d, score, n_obs)) return theta_hat, se def var_irm(theta, g_hat0, g_hat1, m_hat, p_hat, u_hat0, u_hat1, d, score, n_obs): - if score == 'ATE': - var = 1/n_obs * np.mean(np.power(g_hat1 - g_hat0 - + np.divide(np.multiply(d, u_hat1), m_hat) - - np.divide(np.multiply(1.-d, u_hat0), 1.-m_hat) - theta, 2)) + if score == "ATE": + var = ( + 1 + / n_obs + * np.mean( + np.power( + g_hat1 + - g_hat0 + + np.divide(np.multiply(d, u_hat1), m_hat) + - np.divide(np.multiply(1.0 - d, u_hat0), 1.0 - m_hat) + - theta, + 2, + ) + ) + ) else: - assert score == 'ATTE' - var = 1/n_obs * np.mean(np.power(np.divide(np.multiply(d, u_hat0), p_hat) - - np.divide(np.multiply(m_hat, np.multiply(1.-d, u_hat0)), - np.multiply(p_hat, (1.-m_hat))) - - theta * np.divide(d, p_hat), 2)) \ + assert score == "ATTE" + var = ( + 1 + / n_obs + * np.mean( + np.power( + np.divide(np.multiply(d, u_hat0), p_hat) + - np.divide(np.multiply(m_hat, np.multiply(1.0 - d, u_hat0)), np.multiply(p_hat, (1.0 - m_hat))) + - theta * np.divide(d, p_hat), + 2, + ) + ) / np.power(np.mean(np.divide(d, p_hat)), 2) + ) return var def irm_orth(g_hat0, g_hat1, m_hat, p_hat, u_hat0, u_hat1, d, score): - if score == 'ATE': - res = np.mean(g_hat1 - g_hat0 - + np.divide(np.multiply(d, u_hat1), m_hat) - - np.divide(np.multiply(1.-d, u_hat0), 1.-m_hat)) + if score == "ATE": + res = np.mean( + g_hat1 - g_hat0 + np.divide(np.multiply(d, u_hat1), m_hat) - np.divide(np.multiply(1.0 - d, u_hat0), 1.0 - m_hat) + ) else: - assert score == 'ATTE' - res = np.mean(np.divide(np.multiply(d, u_hat0), p_hat) - - np.divide(np.multiply(m_hat, np.multiply(1.-d, u_hat0)), - np.multiply(p_hat, (1.-m_hat)))) \ - / np.mean(np.divide(d, p_hat)) + assert score == "ATTE" + res = np.mean( + np.divide(np.multiply(d, u_hat0), p_hat) + - np.divide(np.multiply(m_hat, np.multiply(1.0 - d, u_hat0)), np.multiply(p_hat, (1.0 - m_hat))) + ) / np.mean(np.divide(d, p_hat)) return res -def boot_irm(y, d, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, all_p_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1, apply_cross_fitting=True, normalize_ipw=True): +def boot_irm( + y, + d, + thetas, + ses, + all_g_hat0, + all_g_hat1, + all_m_hat, + all_p_hat, + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep=1, + apply_cross_fitting=True, + normalize_ipw=True, +): all_boot_t_stat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] @@ -183,9 +229,21 @@ def boot_irm(y, d, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, all_p_hat, n_obs = len(test_index) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_irm_single_split( - thetas[i_rep], y, d, - all_g_hat0[i_rep], all_g_hat1[i_rep], all_m_hat[i_rep], all_p_hat[i_rep], smpls, - score, ses[i_rep], weights, n_rep_boot, apply_cross_fitting, normalize_ipw) + thetas[i_rep], + y, + d, + all_g_hat0[i_rep], + all_g_hat1[i_rep], + all_m_hat[i_rep], + all_p_hat[i_rep], + smpls, + score, + ses[i_rep], + weights, + n_rep_boot, + apply_cross_fitting, + normalize_ipw, + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -193,41 +251,61 @@ def boot_irm(y, d, thetas, ses, all_g_hat0, all_g_hat1, all_m_hat, all_p_hat, return boot_t_stat -def boot_irm_single_split(theta, y, d, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, - smpls, score, se, weights, n_rep_boot, apply_cross_fitting, normalize_ipw): +def boot_irm_single_split( + theta, + y, + d, + g_hat0_list, + g_hat1_list, + m_hat_list, + p_hat_list, + smpls, + score, + se, + weights, + n_rep_boot, + apply_cross_fitting, + normalize_ipw, +): u_hat0, u_hat1, g_hat0, g_hat1, m_hat, p_hat = compute_iivm_residuals( - y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls) + y, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls + ) - m_hat_adj = np.full_like(m_hat, np.nan, dtype='float64') + m_hat_adj = np.full_like(m_hat, np.nan, dtype="float64") if normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, d) else: m_hat_adj = m_hat if apply_cross_fitting: - if score == 'ATE': + if score == "ATE": J = -1.0 else: - assert score == 'ATTE' + assert score == "ATTE" J = np.mean(-np.divide(d, p_hat)) else: test_index = smpls[0][1] - if score == 'ATE': + if score == "ATE": J = -1.0 else: - assert score == 'ATTE' + assert score == "ATTE" J = np.mean(-np.divide(d[test_index], p_hat[test_index])) - if score == 'ATE': - psi = g_hat1 - g_hat0 \ - + np.divide(np.multiply(d, u_hat1), m_hat_adj) \ - - np.divide(np.multiply(1.-d, u_hat0), 1.-m_hat_adj) - theta + if score == "ATE": + psi = ( + g_hat1 + - g_hat0 + + np.divide(np.multiply(d, u_hat1), m_hat_adj) + - np.divide(np.multiply(1.0 - d, u_hat0), 1.0 - m_hat_adj) + - theta + ) else: - assert score == 'ATTE' - psi = np.divide(np.multiply(d, u_hat0), p_hat) \ - - np.divide(np.multiply(m_hat_adj, np.multiply(1.-d, u_hat0)), - np.multiply(p_hat, (1.-m_hat_adj))) \ + assert score == "ATTE" + psi = ( + np.divide(np.multiply(d, u_hat0), p_hat) + - np.divide(np.multiply(m_hat_adj, np.multiply(1.0 - d, u_hat0)), np.multiply(p_hat, (1.0 - m_hat_adj))) - theta * np.divide(d, p_hat) + ) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep_boot, apply_cross_fitting) @@ -245,32 +323,29 @@ def fit_sensitivity_elements_irm(y, d, all_coef, predictions, score, n_rep): for i_rep in range(n_rep): - m_hat = predictions['ml_m'][:, i_rep, 0] - g_hat0 = predictions['ml_g0'][:, i_rep, 0] - g_hat1 = predictions['ml_g1'][:, i_rep, 0] + m_hat = predictions["ml_m"][:, i_rep, 0] + g_hat0 = predictions["ml_g0"][:, i_rep, 0] + g_hat1 = predictions["ml_g1"][:, i_rep, 0] - if score == 'ATE': + if score == "ATE": weights = np.ones_like(d) weights_bar = np.ones_like(d) else: - assert score == 'ATTE' + assert score == "ATTE" weights = np.divide(d, np.mean(d)) weights_bar = np.divide(m_hat, np.mean(d)) - sigma2_score_element = np.square(y - np.multiply(d, g_hat1) - np.multiply(1.0-d, g_hat0)) + sigma2_score_element = np.square(y - np.multiply(d, g_hat1) - np.multiply(1.0 - d, g_hat0)) sigma2[0, i_rep, 0] = np.mean(sigma2_score_element) psi_sigma2[:, i_rep, 0] = sigma2_score_element - sigma2[0, i_rep, 0] # calc m(W,alpha) and Riesz representer - m_alpha = np.multiply(weights, np.multiply(weights_bar, (np.divide(1.0, m_hat) + np.divide(1.0, 1.0-m_hat)))) - rr = np.multiply(weights_bar, (np.divide(d, m_hat) - np.divide(1.0-d, 1.0-m_hat))) + m_alpha = np.multiply(weights, np.multiply(weights_bar, (np.divide(1.0, m_hat) + np.divide(1.0, 1.0 - m_hat)))) + rr = np.multiply(weights_bar, (np.divide(d, m_hat) - np.divide(1.0 - d, 1.0 - m_hat))) nu2_score_element = np.multiply(2.0, m_alpha) - np.square(rr) nu2[0, i_rep, 0] = np.mean(nu2_score_element) psi_nu2[:, i_rep, 0] = nu2_score_element - nu2[0, i_rep, 0] - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2} + element_dict = {"sigma2": sigma2, "nu2": nu2, "psi_sigma2": psi_sigma2, "psi_nu2": psi_nu2} return element_dict diff --git a/doubleml/irm/tests/_utils_lpq_manual.py b/doubleml/irm/tests/_utils_lpq_manual.py index c7a6b9d8a..527f0b395 100644 --- a/doubleml/irm/tests/_utils_lpq_manual.py +++ b/doubleml/irm/tests/_utils_lpq_manual.py @@ -7,14 +7,27 @@ from ...utils._estimation import _default_kde, _dml_cv_predict, _get_bracket_guess, _normalize_ipw, _solve_ipw_score, _trimm -def fit_lpq(y, x, d, z, quantile, - learner_g, learner_m, all_smpls, treatment, n_rep=1, - trimming_rule='truncate', - trimming_threshold=1e-2, - kde=_default_kde, - normalize_ipw=True, m_z_params=None, - m_d_z0_params=None, m_d_z1_params=None, - g_du_z0_params=None, g_du_z1_params=None): +def fit_lpq( + y, + x, + d, + z, + quantile, + learner_g, + learner_m, + all_smpls, + treatment, + n_rep=1, + trimming_rule="truncate", + trimming_threshold=1e-2, + kde=_default_kde, + normalize_ipw=True, + m_z_params=None, + m_d_z0_params=None, + m_d_z1_params=None, + g_du_z0_params=None, + g_du_z1_params=None, +): n_obs = len(y) lpqs = np.zeros(n_rep) @@ -23,34 +36,57 @@ def fit_lpq(y, x, d, z, quantile, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - m_z_hat, g_du_z0_hat, g_du_z1_hat, \ - comp_prob_hat, ipw_vec, coef_bounds = fit_nuisance_lpq(y, x, d, z, quantile, - learner_g, learner_m, smpls, - treatment, - trimming_rule=trimming_rule, - trimming_threshold=trimming_threshold, - normalize_ipw=normalize_ipw, - m_z_params=m_z_params, - m_d_z0_params=m_d_z0_params, - m_d_z1_params=m_d_z1_params, - g_du_z0_params=g_du_z0_params, - g_du_z1_params=g_du_z1_params) - - lpqs[i_rep], ses[i_rep] = lpq_dml2(y, d, z, m_z_hat, g_du_z0_hat, g_du_z1_hat, comp_prob_hat, - treatment, quantile, ipw_vec, coef_bounds, kde) + m_z_hat, g_du_z0_hat, g_du_z1_hat, comp_prob_hat, ipw_vec, coef_bounds = fit_nuisance_lpq( + y, + x, + d, + z, + quantile, + learner_g, + learner_m, + smpls, + treatment, + trimming_rule=trimming_rule, + trimming_threshold=trimming_threshold, + normalize_ipw=normalize_ipw, + m_z_params=m_z_params, + m_d_z0_params=m_d_z0_params, + m_d_z1_params=m_d_z1_params, + g_du_z0_params=g_du_z0_params, + g_du_z1_params=g_du_z1_params, + ) + + lpqs[i_rep], ses[i_rep] = lpq_dml2( + y, d, z, m_z_hat, g_du_z0_hat, g_du_z1_hat, comp_prob_hat, treatment, quantile, ipw_vec, coef_bounds, kde + ) lpq = np.median(lpqs) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(lpqs - lpq, 2)) / n_obs) - res = {'lpq': lpq, 'se': se, - 'lpqs': lpqs, 'ses': ses} + res = {"lpq": lpq, "se": se, "lpqs": lpqs, "ses": ses} return res -def fit_nuisance_lpq(y, x, d, z, quantile, learner_g, learner_m, smpls, treatment, - trimming_rule, trimming_threshold, normalize_ipw, m_z_params, - m_d_z0_params, m_d_z1_params, g_du_z0_params, g_du_z1_params): +def fit_nuisance_lpq( + y, + x, + d, + z, + quantile, + learner_g, + learner_m, + smpls, + treatment, + trimming_rule, + trimming_threshold, + normalize_ipw, + m_z_params, + m_d_z0_params, + m_d_z1_params, + g_du_z0_params, + g_du_z1_params, +): n_folds = len(smpls) n_obs = len(y) # initialize starting values and bounds @@ -90,10 +126,10 @@ def fit_nuisance_lpq(y, x, d, z, quantile, learner_g, learner_m, smpls, treatmen test_inds = smpls[i_fold][1] # start nested crossfitting - train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, - random_state=42, stratify=strata[train_inds]) - smpls_prelim = [(train, test) for train, test in - StratifiedKFold(n_splits=n_folds).split(X=train_inds_1, y=strata[train_inds_1])] + train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, random_state=42, stratify=strata[train_inds]) + smpls_prelim = [ + (train, test) for train, test in StratifiedKFold(n_splits=n_folds).split(X=train_inds_1, y=strata[train_inds_1]) + ] d_train_1 = d[train_inds_1] y_train_1 = y[train_inds_1] @@ -103,8 +139,9 @@ def fit_nuisance_lpq(y, x, d, z, quantile, learner_g, learner_m, smpls, treatmen # preliminary propensity for z # todo change prediction method ml_m_z_prelim = clone(ml_m_z) - m_z_hat_prelim = _dml_cv_predict(ml_m_z_prelim, x_train_1, z_train_1, - method='predict_proba', smpls=smpls_prelim)['preds'] + m_z_hat_prelim = _dml_cv_predict(ml_m_z_prelim, x_train_1, z_train_1, method="predict_proba", smpls=smpls_prelim)[ + "preds" + ] m_z_hat_prelim = _trimm(m_z_hat_prelim, trimming_rule, trimming_threshold) if normalize_ipw: @@ -125,15 +162,18 @@ def fit_nuisance_lpq(y, x, d, z, quantile, learner_g, learner_m, smpls, treatmen m_d_z1_hat_prelim = ml_m_d_z1_prelim.predict_proba(x_train_1)[:, 1] # preliminary estimate of theta_2_aux - comp_prob_prelim = np.mean(m_d_z1_hat_prelim - m_d_z0_hat_prelim - + z_train_1 / m_z_hat_prelim * (d_train_1 - m_d_z1_hat_prelim) - - (1 - z_train_1) / (1 - m_z_hat_prelim) * (d_train_1 - m_d_z0_hat_prelim)) + comp_prob_prelim = np.mean( + m_d_z1_hat_prelim + - m_d_z0_hat_prelim + + z_train_1 / m_z_hat_prelim * (d_train_1 - m_d_z1_hat_prelim) + - (1 - z_train_1) / (1 - m_z_hat_prelim) * (d_train_1 - m_d_z0_hat_prelim) + ) def ipw_score(theta): sign = 2 * treatment - 1.0 weights = sign * (z_train_1 / m_z_hat_prelim - (1 - z_train_1) / (1 - m_z_hat_prelim)) / comp_prob_prelim u = (d_train_1 == treatment) * (y_train_1 <= theta) - v = -1. * quantile + v = -1.0 * quantile res = np.mean(weights * u + v) return res @@ -187,9 +227,9 @@ def ipw_score(theta): m_z_hat = _normalize_ipw(m_z_hat, z) # estimate final nuisance parameter - comp_prob_hat = np.mean(m_d_z1_hat - m_d_z0_hat - + z / m_z_hat * (d - m_d_z1_hat) - - (1 - z) / (1 - m_z_hat) * (d - m_d_z0_hat)) + comp_prob_hat = np.mean( + m_d_z1_hat - m_d_z0_hat + z / m_z_hat * (d - m_d_z1_hat) - (1 - z) / (1 - m_z_hat) * (d - m_d_z0_hat) + ) return m_z_hat, g_du_z0_hat, g_du_z1_hat, comp_prob_hat, ipw_vec, coef_bounds @@ -227,14 +267,12 @@ def get_bracket_guess(coef_start, coef_bounds): b_guess = (a, b) f_a = compute_score(b_guess[0]) f_b = compute_score(b_guess[1]) - s_different = (np.sign(f_a) != np.sign(f_b)) + s_different = np.sign(f_a) != np.sign(f_b) delta += 0.1 return s_different, b_guess _, bracket_guess = get_bracket_guess(ipw_est, coef_bounds) - root_res = root_scalar(compute_score_mean, - bracket=bracket_guess, - method='brentq') + root_res = root_scalar(compute_score_mean, bracket=bracket_guess, method="brentq") dml_est = root_res.root return dml_est @@ -251,15 +289,30 @@ def lpq_var_est(coef, m_z, g_du_z0, g_du_z1, comp_prob, d, y, z, treatment, quan score2 = (z / m_z) * ((d == treatment) * (y <= coef) - g_du_z1) score3 = (1 - z) / (1 - m_z) * ((d == treatment) * (y <= coef) - g_du_z0) score = sign * (score1 + score2 - score3) / comp_prob - quantile - var_est = 1/n_obs * np.mean(np.square(score)) / np.square(J) + var_est = 1 / n_obs * np.mean(np.square(score)) / np.square(J) return var_est -def tune_nuisance_lpq(y, x, d, z, - ml_m_z, ml_m_d_z0, ml_m_d_z1, ml_g_du_z0, ml_g_du_z1, - smpls, treatment, quantile, n_folds_tune, - param_grid_m_z, param_grid_m_d_z0, param_grid_m_d_z1, - param_grid_g_du_z0, param_grid_g_du_z1): +def tune_nuisance_lpq( + y, + x, + d, + z, + ml_m_z, + ml_m_d_z0, + ml_m_d_z1, + ml_g_du_z0, + ml_g_du_z1, + smpls, + treatment, + quantile, + n_folds_tune, + param_grid_m_z, + param_grid_m_d_z0, + param_grid_m_d_z1, + param_grid_g_du_z0, + param_grid_g_du_z1, +): train_cond_z0 = np.where(z == 0)[0] train_cond_z1 = np.where(z == 1)[0] @@ -267,14 +320,10 @@ def tune_nuisance_lpq(y, x, d, z, du = (d == treatment) * (y <= approx_quant) m_z_tune_res = tune_grid_search(z, x, ml_m_z, smpls, param_grid_m_z, n_folds_tune) - m_d_z0_tune_res = tune_grid_search(d, x, ml_m_d_z0, smpls, param_grid_m_d_z0, n_folds_tune, - train_cond=train_cond_z0) - m_d_z1_tune_res = tune_grid_search(d, x, ml_m_d_z1, smpls, param_grid_m_d_z1, n_folds_tune, - train_cond=train_cond_z1) - g_du_z0_tune_res = tune_grid_search(du, x, ml_g_du_z0, smpls, param_grid_g_du_z0, n_folds_tune, - train_cond=train_cond_z0) - g_du_z1_tune_res = tune_grid_search(du, x, ml_g_du_z1, smpls, param_grid_g_du_z1, n_folds_tune, - train_cond=train_cond_z1) + m_d_z0_tune_res = tune_grid_search(d, x, ml_m_d_z0, smpls, param_grid_m_d_z0, n_folds_tune, train_cond=train_cond_z0) + m_d_z1_tune_res = tune_grid_search(d, x, ml_m_d_z1, smpls, param_grid_m_d_z1, n_folds_tune, train_cond=train_cond_z1) + g_du_z0_tune_res = tune_grid_search(du, x, ml_g_du_z0, smpls, param_grid_g_du_z0, n_folds_tune, train_cond=train_cond_z0) + g_du_z1_tune_res = tune_grid_search(du, x, ml_g_du_z1, smpls, param_grid_g_du_z1, n_folds_tune, train_cond=train_cond_z1) m_z_best_params = [xx.best_params_ for xx in m_z_tune_res] m_d_z0_best_params = [xx.best_params_ for xx in m_d_z0_tune_res] diff --git a/doubleml/irm/tests/_utils_pq_manual.py b/doubleml/irm/tests/_utils_pq_manual.py index 44c3cc6bb..3bcd13958 100644 --- a/doubleml/irm/tests/_utils_pq_manual.py +++ b/doubleml/irm/tests/_utils_pq_manual.py @@ -7,9 +7,21 @@ from ...utils._estimation import _default_kde, _dml_cv_predict, _get_bracket_guess, _normalize_ipw, _solve_ipw_score -def fit_pq(y, x, d, quantile, - learner_g, learner_m, all_smpls, treatment, n_rep=1, - trimming_threshold=1e-2, normalize_ipw=True, g_params=None, m_params=None): +def fit_pq( + y, + x, + d, + quantile, + learner_g, + learner_m, + all_smpls, + treatment, + n_rep=1, + trimming_threshold=1e-2, + normalize_ipw=True, + g_params=None, + m_params=None, +): n_obs = len(y) pqs = np.zeros(n_rep) @@ -18,25 +30,34 @@ def fit_pq(y, x, d, quantile, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat, ipw_est = fit_nuisance_pq(y, x, d, quantile, - learner_g, learner_m, smpls, treatment, - trimming_threshold=trimming_threshold, - normalize_ipw=normalize_ipw, - g_params=g_params, m_params=m_params) + g_hat, m_hat, ipw_est = fit_nuisance_pq( + y, + x, + d, + quantile, + learner_g, + learner_m, + smpls, + treatment, + trimming_threshold=trimming_threshold, + normalize_ipw=normalize_ipw, + g_params=g_params, + m_params=m_params, + ) pqs[i_rep], ses[i_rep] = pq_dml2(y, d, g_hat, m_hat, treatment, quantile, ipw_est) pq = np.median(pqs) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(pqs - pq, 2)) / n_obs) - res = {'pq': pq, 'se': se, - 'pqs': pqs, 'ses': ses} + res = {"pq": pq, "se": se, "pqs": pqs, "ses": ses} return res -def fit_nuisance_pq(y, x, d, quantile, learner_g, learner_m, smpls, treatment, - trimming_threshold, normalize_ipw, g_params, m_params): +def fit_nuisance_pq( + y, x, d, quantile, learner_g, learner_m, smpls, treatment, trimming_threshold, normalize_ipw, g_params, m_params +): n_folds = len(smpls) n_obs = len(y) # initialize starting values and bounds @@ -62,18 +83,17 @@ def fit_nuisance_pq(y, x, d, quantile, learner_g, learner_m, smpls, treatment, test_inds = smpls[i_fold][1] # start nested crossfitting - train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, - random_state=42, stratify=d[train_inds]) - smpls_prelim = [(train, test) for train, test in - StratifiedKFold(n_splits=n_folds).split(X=train_inds_1, y=d[train_inds_1])] + train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, random_state=42, stratify=d[train_inds]) + smpls_prelim = [ + (train, test) for train, test in StratifiedKFold(n_splits=n_folds).split(X=train_inds_1, y=d[train_inds_1]) + ] d_train_1 = d[train_inds_1] y_train_1 = y[train_inds_1] x_train_1 = x[train_inds_1, :] # todo change prediction method - m_hat_prelim = _dml_cv_predict(clone(ml_m), x_train_1, d_train_1, - method='predict_proba', smpls=smpls_prelim)['preds'] + m_hat_prelim = _dml_cv_predict(clone(ml_m), x_train_1, d_train_1, method="predict_proba", smpls=smpls_prelim)["preds"] m_hat_prelim[m_hat_prelim < trimming_threshold] = trimming_threshold m_hat_prelim[m_hat_prelim > 1 - trimming_threshold] = 1 - trimming_threshold @@ -146,7 +166,7 @@ def get_bracket_guess(coef_start, coef_bounds): b_guess = (a, b) f_a = compute_score(b_guess[0]) f_b = compute_score(b_guess[1]) - s_different = (np.sign(f_a) != np.sign(f_b)) + s_different = np.sign(f_a) != np.sign(f_b) delta += 0.1 return s_different, b_guess @@ -154,9 +174,7 @@ def get_bracket_guess(coef_start, coef_bounds): coef_bounds = (y.min(), y.max()) _, bracket_guess = get_bracket_guess(coef_start_val, coef_bounds) - root_res = root_scalar(compute_score, - bracket=bracket_guess, - method='brentq') + root_res = root_scalar(compute_score, bracket=bracket_guess, method="brentq") dml_est = root_res.root return dml_est @@ -169,16 +187,14 @@ def pq_var_est(coef, g_hat, m_hat, d, y, treatment, quantile, n_obs, kde=_defaul J = np.mean(deriv) score = (d == treatment) * ((y <= coef) - g_hat) / m_hat + g_hat - quantile - var_est = 1/n_obs * np.mean(np.square(score)) / np.square(J) + var_est = 1 / n_obs * np.mean(np.square(score)) / np.square(J) return var_est -def tune_nuisance_pq(y, x, d, ml_g, ml_m, smpls, treatment, quantile, n_folds_tune, - param_grid_g, param_grid_m): +def tune_nuisance_pq(y, x, d, ml_g, ml_m, smpls, treatment, quantile, n_folds_tune, param_grid_g, param_grid_m): train_cond_treat = np.where(d == treatment)[0] approx_goal = y <= np.quantile(y[d == treatment], quantile) - g_tune_res = tune_grid_search(approx_goal, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=train_cond_treat) + g_tune_res = tune_grid_search(approx_goal, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=train_cond_treat) m_tune_res = tune_grid_search(d, x, ml_m, smpls, param_grid_m, n_folds_tune) g_best_params = [xx.best_params_ for xx in g_tune_res] diff --git a/doubleml/irm/tests/_utils_qte_manual.py b/doubleml/irm/tests/_utils_qte_manual.py index ea742b407..aed4fdae6 100644 --- a/doubleml/irm/tests/_utils_qte_manual.py +++ b/doubleml/irm/tests/_utils_qte_manual.py @@ -7,9 +7,21 @@ from ..pq import DoubleMLPQ -def fit_qte(y, x, d, quantiles, learner_g, learner_m, all_smpls, n_rep=1, - trimming_rule='truncate', trimming_threshold=1e-2, kde=_default_kde, - normalize_ipw=True, draw_sample_splitting=True): +def fit_qte( + y, + x, + d, + quantiles, + learner_g, + learner_m, + all_smpls, + n_rep=1, + trimming_rule="truncate", + trimming_threshold=1e-2, + kde=_default_kde, + normalize_ipw=True, + draw_sample_splitting=True, +): n_obs = len(y) n_quantiles = len(quantiles) @@ -24,30 +36,34 @@ def fit_qte(y, x, d, quantiles, learner_g, learner_m, all_smpls, n_rep=1, for i_quant in range(n_quantiles): # initialize models for both potential quantiles - model_PQ_0 = DoubleMLPQ(dml_data, - clone(learner_g), - clone(learner_m), - quantile=quantiles[i_quant], - treatment=0, - n_folds=n_folds, - n_rep=n_rep, - trimming_rule=trimming_rule, - trimming_threshold=trimming_threshold, - kde=kde, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) - model_PQ_1 = DoubleMLPQ(dml_data, - clone(learner_g), - clone(learner_m), - quantile=quantiles[i_quant], - treatment=1, - n_folds=n_folds, - n_rep=n_rep, - trimming_rule=trimming_rule, - trimming_threshold=trimming_threshold, - kde=kde, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) + model_PQ_0 = DoubleMLPQ( + dml_data, + clone(learner_g), + clone(learner_m), + quantile=quantiles[i_quant], + treatment=0, + n_folds=n_folds, + n_rep=n_rep, + trimming_rule=trimming_rule, + trimming_threshold=trimming_threshold, + kde=kde, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + ) + model_PQ_1 = DoubleMLPQ( + dml_data, + clone(learner_g), + clone(learner_m), + quantile=quantiles[i_quant], + treatment=1, + n_folds=n_folds, + n_rep=n_rep, + trimming_rule=trimming_rule, + trimming_threshold=trimming_threshold, + kde=kde, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + ) # synchronize the sample splitting model_PQ_0.set_sample_splitting(all_smpls) @@ -75,12 +91,11 @@ def fit_qte(y, x, d, quantiles, learner_g, learner_m, all_smpls, n_rep=1, qte = np.median(qtes, 1) se = np.zeros(n_quantiles) for i_quant in range(n_quantiles): - se[i_quant] = np.sqrt(np.median(np.power(ses[i_quant, :], 2) * n_obs + - np.power(qtes[i_quant, :] - qte[i_quant], 2)) / n_obs) + se[i_quant] = np.sqrt( + np.median(np.power(ses[i_quant, :], 2) * n_obs + np.power(qtes[i_quant, :] - qte[i_quant], 2)) / n_obs + ) - res = {'qte': qte, 'se': se, - 'qtes': qtes, 'ses': ses, - 'scaled_scores': scaled_scores} + res = {"qte": qte, "se": se, "qtes": qtes, "ses": ses, "scaled_scores": scaled_scores} return res @@ -92,7 +107,8 @@ def boot_qte(scaled_scores, ses, quantiles, all_smpls, n_rep, bootstrap, n_rep_b n_obs = scaled_scores.shape[0] weights = draw_weights(bootstrap, n_rep_boot, n_obs) for i_quant in range(n_quantiles): - boot_t_stat[:, i_quant, i_rep] = np.matmul(weights, scaled_scores[:, i_quant, i_rep]) / \ - (n_obs * ses[i_quant, i_rep]) + boot_t_stat[:, i_quant, i_rep] = np.matmul(weights, scaled_scores[:, i_quant, i_rep]) / ( + n_obs * ses[i_quant, i_rep] + ) return boot_t_stat diff --git a/doubleml/irm/tests/_utils_ssm_manual.py b/doubleml/irm/tests/_utils_ssm_manual.py index 2d3683191..d0aa33ed8 100644 --- a/doubleml/irm/tests/_utils_ssm_manual.py +++ b/doubleml/irm/tests/_utils_ssm_manual.py @@ -6,15 +6,26 @@ from ...utils._estimation import _predict_zero_one_propensity, _trimm -def fit_selection(y, x, d, z, s, - learner_g, learner_pi, learner_m, - all_smpls, score, - trimming_rule='truncate', - trimming_threshold=1e-2, - normalize_ipw=True, - n_rep=1, - g_d0_params=None, g_d1_params=None, - pi_params=None, m_params=None): +def fit_selection( + y, + x, + d, + z, + s, + learner_g, + learner_pi, + learner_m, + all_smpls, + score, + trimming_rule="truncate", + trimming_threshold=1e-2, + normalize_ipw=True, + n_rep=1, + g_d0_params=None, + g_d1_params=None, + pi_params=None, + m_params=None, +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -31,14 +42,24 @@ def fit_selection(y, x, d, z, s, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat_d1_list, g_hat_d0_list, pi_hat_list, \ - m_hat_list = fit_nuisance_selection(y, x, d, z, s, - learner_g, learner_pi, learner_m, - smpls, score, - trimming_rule=trimming_rule, - trimming_threshold=trimming_threshold, - g_d0_params=g_d0_params, g_d1_params=g_d1_params, - pi_params=pi_params, m_params=m_params) + g_hat_d1_list, g_hat_d0_list, pi_hat_list, m_hat_list = fit_nuisance_selection( + y, + x, + d, + z, + s, + learner_g, + learner_pi, + learner_m, + smpls, + score, + trimming_rule=trimming_rule, + trimming_threshold=trimming_threshold, + g_d0_params=g_d0_params, + g_d1_params=g_d1_params, + pi_params=pi_params, + m_params=m_params, + ) all_g_d1_hat.append(g_hat_d1_list) all_g_d0_hat.append(g_hat_d0_list) all_pi_hat.append(pi_hat_list) @@ -46,10 +67,9 @@ def fit_selection(y, x, d, z, s, g_hat_d1, g_hat_d0, pi_hat, m_hat = compute_selection(y, g_hat_d1_list, g_hat_d0_list, pi_hat_list, m_hat_list, smpls) - dtreat = (d == 1) - dcontrol = (d == 0) - psi_a, psi_b = selection_score_elements(dtreat, dcontrol, g_hat_d1, g_hat_d0, pi_hat, m_hat, - s, y, normalize_ipw) + dtreat = d == 1 + dcontrol = d == 0 + psi_a, psi_b = selection_score_elements(dtreat, dcontrol, g_hat_d1, g_hat_d0, pi_hat, m_hat, s, y, normalize_ipw) all_psi_a.append(psi_a) all_psi_b.append(psi_b) @@ -59,22 +79,40 @@ def fit_selection(y, x, d, z, s, theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_g_d1_hat': all_g_d1_hat, 'all_g_d0_hat': all_g_d0_hat, - 'all_pi_hat': all_pi_hat, 'all_m_hat': all_m_hat, - 'all_psi_a': all_psi_a, 'all_psi_b': all_psi_b} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_g_d1_hat": all_g_d1_hat, + "all_g_d0_hat": all_g_d0_hat, + "all_pi_hat": all_pi_hat, + "all_m_hat": all_m_hat, + "all_psi_a": all_psi_a, + "all_psi_b": all_psi_b, + } return res -def fit_nuisance_selection(y, x, d, z, s, - learner_g, learner_pi, learner_m, - smpls, score, - trimming_rule='truncate', - trimming_threshold=1e-2, - g_d0_params=None, g_d1_params=None, - pi_params=None, m_params=None): +def fit_nuisance_selection( + y, + x, + d, + z, + s, + learner_g, + learner_pi, + learner_m, + smpls, + score, + trimming_rule="truncate", + trimming_threshold=1e-2, + g_d0_params=None, + g_d1_params=None, + pi_params=None, + m_params=None, +): ml_g_d1 = clone(learner_g) ml_g_d0 = clone(learner_g) @@ -86,7 +124,7 @@ def fit_nuisance_selection(y, x, d, z, s, else: dx = np.column_stack((d, x, z)) - if score == 'missing-at-random': + if score == "missing-at-random": pi_hat_list = fit_predict_proba(s, dx, ml_pi, pi_params, smpls, trimming_threshold=trimming_threshold) m_hat_list = fit_predict_proba(d, x, ml_m, m_params, smpls) @@ -126,8 +164,9 @@ def fit_nuisance_selection(y, x, d, z, s, test_inds = smpls[i_fold][1] # start nested crossfitting - train_inds_1, train_inds_2 = train_test_split(train_inds, test_size=0.5, - random_state=42, stratify=strata[train_inds]) + train_inds_1, train_inds_2 = train_test_split( + train_inds, test_size=0.5, random_state=42, stratify=strata[train_inds] + ) s_train_1 = s[train_inds_1] dx_train_1 = dx[train_inds_1, :] @@ -154,8 +193,7 @@ def fit_nuisance_selection(y, x, d, z, s, m_hat = _predict_zero_one_propensity(ml_m, xpi_test) # estimate conditional outcome on second training sample -- treatment - s1_d1_train_2_indices = np.intersect1d(np.where(d == 1)[0], - np.intersect1d(np.where(s == 1)[0], train_inds_2)) + s1_d1_train_2_indices = np.intersect1d(np.where(d == 1)[0], np.intersect1d(np.where(s == 1)[0], train_inds_2)) xpi_s1_d1_train_2 = xpi[s1_d1_train_2_indices, :] y_s1_d1_train_2 = y[s1_d1_train_2_indices] @@ -165,8 +203,7 @@ def fit_nuisance_selection(y, x, d, z, s, g_hat_d1 = ml_g_d1.predict(xpi_test) # estimate conditional outcome on second training sample -- control - s1_d0_train_2_indices = np.intersect1d(np.where(d == 0)[0], - np.intersect1d(np.where(s == 1)[0], train_inds_2)) + s1_d0_train_2_indices = np.intersect1d(np.where(d == 0)[0], np.intersect1d(np.where(s == 1)[0], train_inds_2)) xpi_s1_d0_train_2 = xpi[s1_d0_train_2_indices, :] y_s1_d0_train_2 = y[s1_d0_train_2_indices] @@ -187,10 +224,10 @@ def fit_nuisance_selection(y, x, d, z, s, def compute_selection(y, g_hat_d1_list, g_hat_d0_list, pi_hat_list, m_hat_list, smpls): - g_hat_d1 = np.full_like(y, np.nan, dtype='float64') - g_hat_d0 = np.full_like(y, np.nan, dtype='float64') - pi_hat = np.full_like(y, np.nan, dtype='float64') - m_hat = np.full_like(y, np.nan, dtype='float64') + g_hat_d1 = np.full_like(y, np.nan, dtype="float64") + g_hat_d0 = np.full_like(y, np.nan, dtype="float64") + pi_hat = np.full_like(y, np.nan, dtype="float64") + m_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): g_hat_d1[test_index] = g_hat_d1_list[idx] @@ -201,8 +238,7 @@ def compute_selection(y, g_hat_d1_list, g_hat_d0_list, pi_hat_list, m_hat_list, return g_hat_d1, g_hat_d0, pi_hat, m_hat -def selection_score_elements(dtreat, dcontrol, g_d1, g_d0, - pi, m, s, y, normalize_ipw): +def selection_score_elements(dtreat, dcontrol, g_d1, g_d0, pi, m, s, y, normalize_ipw): # psi_a psi_a = -1 * np.ones_like(y) @@ -225,7 +261,7 @@ def selection_score_elements(dtreat, dcontrol, g_d1, g_d0, def selection_dml2(psi_a, psi_b): n_obs = len(psi_a) - theta_hat = - np.mean(psi_b) / np.mean(psi_a) + theta_hat = -np.mean(psi_b) / np.mean(psi_a) se = np.sqrt(var_selection(theta_hat, psi_a, psi_b, n_obs)) return theta_hat, se @@ -233,21 +269,18 @@ def selection_dml2(psi_a, psi_b): def var_selection(theta, psi_a, psi_b, n_obs): J = np.mean(psi_a) - var = 1/n_obs * np.mean(np.power(np.multiply(psi_a, theta) + psi_b, 2)) / np.power(J, 2) + var = 1 / n_obs * np.mean(np.power(np.multiply(psi_a, theta) + psi_b, 2)) / np.power(J, 2) return var -def tune_nuisance_ssm(y, x, d, z, s, ml_g, ml_pi, ml_m, smpls, score, n_folds_tune, - param_grid_g, param_grid_pi, param_grid_m): +def tune_nuisance_ssm(y, x, d, z, s, ml_g, ml_pi, ml_m, smpls, score, n_folds_tune, param_grid_g, param_grid_pi, param_grid_m): d0_s1 = np.intersect1d(np.where(d == 0)[0], np.where(s == 1)[0]) d1_s1 = np.intersect1d(np.where(d == 1)[0], np.where(s == 1)[0]) - g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=d0_s1) - g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, - train_cond=d1_s1) + g0_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=d0_s1) + g1_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune, train_cond=d1_s1) - if score == 'nonignorable': + if score == "nonignorable": dx = np.column_stack((x, d, z)) else: dx = np.column_stack((x, d)) diff --git a/doubleml/irm/tests/conftest.py b/doubleml/irm/tests/conftest.py index 4e5b07f99..1cf1d5250 100644 --- a/doubleml/irm/tests/conftest.py +++ b/doubleml/irm/tests/conftest.py @@ -11,9 +11,7 @@ def _g(x): return np.power(np.sin(x), 2) -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20)]) def generate_data_irm(request): n_p = request.param np.random.seed(1111) @@ -23,15 +21,12 @@ def generate_data_irm(request): theta = 0.5 # generating data - data = make_irm_data(n, p, theta, return_type='array') + data = make_irm_data(n, p, theta, return_type="array") return data -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20), - (1000, 100)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20), (1000, 100)]) def generate_data_irm_binary(request): n_p = request.param np.random.seed(1111) @@ -43,21 +38,51 @@ def generate_data_irm_binary(request): sigma = make_spd_matrix(p) # generating data - x = np.random.multivariate_normal(np.zeros(p), sigma, size=[n, ]) + x = np.random.multivariate_normal( + np.zeros(p), + sigma, + size=[ + n, + ], + ) G = _g(np.dot(x, b)) - pr = 1 / (1 + np.exp((-1) * (x[:, 0] * (-0.5) + x[:, 1] * 0.5 + np.random.standard_normal(size=[n, ])))) - d = np.random.binomial(p=pr, n=1, size=[n, ]) + pr = 1 / ( + 1 + + np.exp( + (-1) + * ( + x[:, 0] * (-0.5) + + x[:, 1] * 0.5 + + np.random.standard_normal( + size=[ + n, + ] + ) + ) + ) + ) + d = np.random.binomial( + p=pr, + n=1, + size=[ + n, + ], + ) err = np.random.standard_normal(n) pry = 1 / (1 + np.exp((-1) * theta * d + G + err)) - y = np.random.binomial(p=pry, n=1, size=[n, ]) + y = np.random.binomial( + p=pry, + n=1, + size=[ + n, + ], + ) return x, y, d -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20)]) def generate_data_irm_w_missings(request): n_p = request.param np.random.seed(1111) @@ -67,19 +92,17 @@ def generate_data_irm_w_missings(request): theta = 0.5 # generating data - (x, y, d) = make_irm_data(n, p, theta, return_type='array') + (x, y, d) = make_irm_data(n, p, theta, return_type="array") # randomly set some entries to np.nan - ind = np.random.choice(np.arange(x.size), replace=False, - size=int(x.size * 0.05)) + ind = np.random.choice(np.arange(x.size), replace=False, size=int(x.size * 0.05)) x[np.unravel_index(ind, x.shape)] = np.nan data = (x, y, d) return data -@pytest.fixture(scope='session', - params=[(500, 11)]) +@pytest.fixture(scope="session", params=[(500, 11)]) def generate_data_iivm(request): n_p = request.param np.random.seed(1111) @@ -95,10 +118,7 @@ def generate_data_iivm(request): return data -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20), - (1000, 100)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20), (1000, 100)]) def generate_data_iivm_binary(request): n_p = request.param np.random.seed(1111) @@ -110,26 +130,81 @@ def generate_data_iivm_binary(request): sigma = make_spd_matrix(p) # generating data - x = np.random.multivariate_normal(np.zeros(p), sigma, size=[n, ]) + x = np.random.multivariate_normal( + np.zeros(p), + sigma, + size=[ + n, + ], + ) G = _g(np.dot(x, b)) - prz = 1 / (1 + np.exp((-1) * (x[:, 0] * (-1) * b[4] + x[:, 1] * b[2] + np.random.standard_normal(size=[n, ])))) - z = np.random.binomial(p=prz, n=1, size=[n, ]) - u = np.random.standard_normal(size=[n, ]) - pr = 1 / (1 + np.exp((-1) * (0.5 * z + x[:, 0] * (-0.5) + x[:, 1] * 0.25 - 0.5 * u - + np.random.standard_normal(size=[n, ])))) - d = np.random.binomial(p=pr, n=1, size=[n, ]) + prz = 1 / ( + 1 + + np.exp( + (-1) + * ( + x[:, 0] * (-1) * b[4] + + x[:, 1] * b[2] + + np.random.standard_normal( + size=[ + n, + ] + ) + ) + ) + ) + z = np.random.binomial( + p=prz, + n=1, + size=[ + n, + ], + ) + u = np.random.standard_normal( + size=[ + n, + ] + ) + pr = 1 / ( + 1 + + np.exp( + (-1) + * ( + 0.5 * z + + x[:, 0] * (-0.5) + + x[:, 1] * 0.25 + - 0.5 * u + + np.random.standard_normal( + size=[ + n, + ] + ) + ) + ) + ) + d = np.random.binomial( + p=pr, + n=1, + size=[ + n, + ], + ) err = np.random.standard_normal(n) pry = 1 / (1 + np.exp((-1) * theta * d + G + 4 * u + err)) - y = np.random.binomial(p=pry, n=1, size=[n, ]) + y = np.random.binomial( + p=pry, + n=1, + size=[ + n, + ], + ) return x, y, d, z -@pytest.fixture(scope='session', - params=[(500, 5), - (1000, 10)]) +@pytest.fixture(scope="session", params=[(500, 5), (1000, 10)]) def generate_data_quantiles(request): n_p = request.param np.random.seed(1111) @@ -156,9 +231,7 @@ def f_scale(D, X): return data -@pytest.fixture(scope='session', - params=[(5000, 5), - (10000, 10)]) +@pytest.fixture(scope="session", params=[(5000, 5), (10000, 10)]) def generate_data_local_quantiles(request): n_p = request.param np.random.seed(1111) @@ -186,15 +259,13 @@ def generate_treatment(Z, X, X_conf): d = generate_treatment(z, x, x_conf) epsilon = np.random.normal(size=n) - y = f_loc(d, x, x_conf) + f_scale(d, x, x_conf)*epsilon + y = f_loc(d, x, x_conf) + f_scale(d, x, x_conf) * epsilon data = (x, y, d, z) return data -@pytest.fixture(scope='session', - params=[(8000, 2), - (16000, 5)]) +@pytest.fixture(scope="session", params=[(8000, 2), (16000, 5)]) def generate_data_selection_mar(request): params = request.param np.random.seed(1111) @@ -206,7 +277,13 @@ def generate_data_selection_mar(request): e = np.random.multivariate_normal(mean=[0, 0], cov=sigma, size=n_obs).T cov_mat = toeplitz([np.power(0.5, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) beta = [0.4 / (k**2) for k in range(1, dim_x + 1)] @@ -222,9 +299,7 @@ def generate_data_selection_mar(request): return data -@pytest.fixture(scope='session', - params=[(8000, 2), - (16000, 5)]) +@pytest.fixture(scope="session", params=[(8000, 2), (16000, 5)]) def generate_data_selection_nonignorable(request): params = request.param np.random.seed(1111) @@ -237,7 +312,13 @@ def generate_data_selection_nonignorable(request): e = np.random.multivariate_normal(mean=[0, 0], cov=sigma, size=n_obs).T cov_mat = toeplitz([np.power(0.5, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) beta = [0.4 / (k**2) for k in range(1, dim_x + 1)] diff --git a/doubleml/irm/tests/test_apo.py b/doubleml/irm/tests/test_apo.py index 806c7dc5c..920f60476 100644 --- a/doubleml/irm/tests/test_apo.py +++ b/doubleml/irm/tests/test_apo.py @@ -14,36 +14,38 @@ from ._utils_apo_manual import boot_apo, fit_apo, fit_sensitivity_elements_apo -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250, random_state=42)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250, random_state=42)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment_level(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_apo_fixture(generate_data_irm, learner, normalize_ipw, trimming_threshold, treatment_level): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -54,153 +56,169 @@ def dml_apo_fixture(generate_data_irm, learner, normalize_ipw, trimming_threshol np.random.seed(3141) n_obs = 500 data_apo = make_irm_data_discrete_treatments(n_obs=n_obs) - y = data_apo['y'] - x = data_apo['x'] - d = data_apo['d'] + y = data_apo["y"] + x = data_apo["x"] + d = data_apo["d"] df_apo = pd.DataFrame( - np.column_stack((y, d, x)), - columns=['y', 'd'] + ['x' + str(i) for i in range(data_apo['x'].shape[1])] + np.column_stack((y, d, x)), columns=["y", "d"] + ["x" + str(i) for i in range(data_apo["x"].shape[1])] ) - dml_data = dml.DoubleMLData(df_apo, 'y', 'd') + dml_data = dml.DoubleMLData(df_apo, "y", "d") all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d) np.random.seed(3141) - dml_obj = dml.DoubleMLAPO(dml_data, - ml_g, ml_m, - treatment_level=treatment_level, - n_folds=n_folds, - score='APO', - normalize_ipw=normalize_ipw, - draw_sample_splitting=False, - trimming_threshold=trimming_threshold) + dml_obj = dml.DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=treatment_level, + n_folds=n_folds, + score="APO", + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_obj.set_sample_splitting(all_smpls=all_smpls) dml_obj.fit() np.random.seed(3141) - res_manual = fit_apo(y, x, d, - clone(learner[0]), clone(learner[1]), - treatment_level=treatment_level, - all_smpls=all_smpls, - score='APO', - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + res_manual = fit_apo( + y, + x, + d, + clone(learner[0]), + clone(learner[1]), + treatment_level=treatment_level, + all_smpls=all_smpls, + score="APO", + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) np.random.seed(3141) # test with external nuisance predictions - dml_obj_ext = dml.DoubleMLAPO(dml_data, - ml_g, ml_m, - treatment_level=treatment_level, - n_folds=n_folds, - score='APO', - normalize_ipw=normalize_ipw, - draw_sample_splitting=False, - trimming_threshold=trimming_threshold) + dml_obj_ext = dml.DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=treatment_level, + n_folds=n_folds, + score="APO", + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_obj_ext.set_sample_splitting(all_smpls=all_smpls) - prediction_dict = {'d': {'ml_g0': dml_obj.predictions['ml_g0'].reshape(-1, 1), - 'ml_g1': dml_obj.predictions['ml_g1'].reshape(-1, 1), - 'ml_m': dml_obj.predictions['ml_m'].reshape(-1, 1)}} + prediction_dict = { + "d": { + "ml_g0": dml_obj.predictions["ml_g0"].reshape(-1, 1), + "ml_g1": dml_obj.predictions["ml_g1"].reshape(-1, 1), + "ml_m": dml_obj.predictions["ml_m"].reshape(-1, 1), + } + } dml_obj_ext.fit(external_predictions=prediction_dict) - res_dict = {'coef': dml_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'coef_ext': dml_obj_ext.coef.item(), - 'se': dml_obj.se.item(), - 'se_manual': res_manual['se'], - 'se_ext': dml_obj_ext.se.item(), - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_obj.coef.item(), + "coef_manual": res_manual["theta"], + "coef_ext": dml_obj_ext.coef.item(), + "se": dml_obj.se.item(), + "se_manual": res_manual["se"], + "se_ext": dml_obj_ext.se.item(), + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_apo(y, d, treatment_level, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], - all_smpls, - score='APO', - bootstrap=bootstrap, - n_rep_boot=n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_apo( + y, + d, + treatment_level, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + all_smpls, + score="APO", + bootstrap=bootstrap, + n_rep_boot=n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) np.random.seed(3141) dml_obj_ext.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) - res_dict['boot_t_stat' + bootstrap + '_ext'] = dml_obj_ext.boot_t_stat + res_dict["boot_t_stat" + bootstrap] = dml_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap + "_ext"] = dml_obj_ext.boot_t_stat # check if sensitivity score with rho=0 gives equal asymptotic standard deviation dml_obj.sensitivity_analysis(rho=0.0) - res_dict['sensitivity_ses'] = dml_obj.sensitivity_params['se'] + res_dict["sensitivity_ses"] = dml_obj.sensitivity_params["se"] # sensitivity tests - res_dict['sensitivity_elements'] = dml_obj.sensitivity_elements - res_dict['sensitivity_elements_manual'] = fit_sensitivity_elements_apo(y, d, - treatment_level, - all_coef=dml_obj.all_coef, - predictions=dml_obj.predictions, - score='APO', - n_rep=1) + res_dict["sensitivity_elements"] = dml_obj.sensitivity_elements + res_dict["sensitivity_elements_manual"] = fit_sensitivity_elements_apo( + y, d, treatment_level, all_coef=dml_obj.all_coef, predictions=dml_obj.predictions, score="APO", n_rep=1 + ) return res_dict @pytest.mark.ci def test_dml_apo_coef(dml_apo_fixture): - assert math.isclose(dml_apo_fixture['coef'], - dml_apo_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_apo_fixture['coef'], - dml_apo_fixture['coef_ext'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_fixture["coef"], dml_apo_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_fixture["coef"], dml_apo_fixture["coef_ext"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_apo_se(dml_apo_fixture): - assert math.isclose(dml_apo_fixture['se'], - dml_apo_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_apo_fixture['se'], - dml_apo_fixture['se_ext'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_fixture["se"], dml_apo_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_fixture["se"], dml_apo_fixture["se_ext"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_apo_boot(dml_apo_fixture): - for bootstrap in dml_apo_fixture['boot_methods']: - assert np.allclose(dml_apo_fixture['boot_t_stat' + bootstrap], - dml_apo_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_apo_fixture['boot_t_stat' + bootstrap], - dml_apo_fixture['boot_t_stat' + bootstrap + '_ext'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apo_fixture["boot_methods"]: + assert np.allclose( + dml_apo_fixture["boot_t_stat" + bootstrap], + dml_apo_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) + assert np.allclose( + dml_apo_fixture["boot_t_stat" + bootstrap], + dml_apo_fixture["boot_t_stat" + bootstrap + "_ext"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_apo_sensitivity_rho0(dml_apo_fixture): - assert np.allclose(dml_apo_fixture['se'], - dml_apo_fixture['sensitivity_ses']['lower'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_apo_fixture['se'], - dml_apo_fixture['sensitivity_ses']['upper'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_apo_fixture["se"], dml_apo_fixture["sensitivity_ses"]["lower"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_apo_fixture["se"], dml_apo_fixture["sensitivity_ses"]["upper"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_apo_sensitivity(dml_apo_fixture): - sensitivity_element_names = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2'] + sensitivity_element_names = ["sigma2", "nu2", "psi_sigma2", "psi_nu2"] for sensitivity_element in sensitivity_element_names: - assert np.allclose(dml_apo_fixture['sensitivity_elements'][sensitivity_element], - dml_apo_fixture['sensitivity_elements_manual'][sensitivity_element], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_apo_fixture["sensitivity_elements"][sensitivity_element], + dml_apo_fixture["sensitivity_elements_manual"][sensitivity_element], + rtol=1e-9, + atol=1e-4, + ) -@pytest.fixture(scope='module', - params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) +@pytest.fixture(scope="module", params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) def cov_type(request): return request.param @@ -216,12 +234,9 @@ def test_dml_apo_capo_gapo(treatment_level, cov_type): ml_g = RandomForestRegressor(n_estimators=10) ml_m = RandomForestClassifier(n_estimators=10) - dml_obj = dml.DoubleMLAPO(obj_dml_data, - ml_m=ml_m, - ml_g=ml_g, - treatment_level=treatment_level, - trimming_threshold=0.05, - n_folds=5) + dml_obj = dml.DoubleMLAPO( + obj_dml_data, ml_m=ml_m, ml_g=ml_g, treatment_level=treatment_level, trimming_threshold=0.05, n_folds=5 + ) dml_obj.fit() # create a random basis @@ -231,10 +246,10 @@ def test_dml_apo_capo_gapo(treatment_level, cov_type): assert isinstance(capo.confint(), pd.DataFrame) assert capo.blp_model.cov_type == cov_type - groups_1 = pd.DataFrame(np.column_stack([obj_dml_data.data['X1'] <= -1.0, - obj_dml_data.data['X1'] > 0.2]), - columns=['Group 1', 'Group 2']) - msg = ('At least one group effect is estimated with less than 6 observations.') + groups_1 = pd.DataFrame( + np.column_stack([obj_dml_data.data["X1"] <= -1.0, obj_dml_data.data["X1"] > 0.2]), columns=["Group 1", "Group 2"] + ) + msg = "At least one group effect is estimated with less than 6 observations." with pytest.warns(UserWarning, match=msg): gapo_1 = dml_obj.gapo(groups_1, cov_type=cov_type) assert isinstance(gapo_1, dml.utils.blp.DoubleMLBLP) @@ -244,7 +259,7 @@ def test_dml_apo_capo_gapo(treatment_level, cov_type): np.random.seed(42) groups_2 = pd.DataFrame(np.random.choice(["1", "2"], n, p=[0.1, 0.9])) - msg = ('At least one group effect is estimated with less than 6 observations.') + msg = "At least one group effect is estimated with less than 6 observations." with pytest.warns(UserWarning, match=msg): gapo_2 = dml_obj.gapo(groups_2, cov_type=cov_type) assert isinstance(gapo_2, dml.utils.blp.DoubleMLBLP) diff --git a/doubleml/irm/tests/test_apo_classifier.py b/doubleml/irm/tests/test_apo_classifier.py index ca5be5ae3..042f3fe84 100644 --- a/doubleml/irm/tests/test_apo_classifier.py +++ b/doubleml/irm/tests/test_apo_classifier.py @@ -12,30 +12,33 @@ from ._utils_apo_manual import boot_apo, fit_apo -@pytest.fixture(scope='module', - params=[[LogisticRegression(solver='lbfgs', max_iter=250), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LogisticRegression(solver="lbfgs", max_iter=250), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_apo_classifier_fixture(generate_data_irm_binary, learner, normalize_ipw, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -53,64 +56,87 @@ def dml_apo_classifier_fixture(generate_data_irm_binary, learner, normalize_ipw, np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) - dml_obj = dml.DoubleMLAPO(obj_dml_data, - ml_g, ml_m, - treatment_level=treatment_level, - n_folds=n_folds, - score=score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + dml_obj = dml.DoubleMLAPO( + obj_dml_data, + ml_g, + ml_m, + treatment_level=treatment_level, + n_folds=n_folds, + score=score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_obj.set_sample_splitting(all_smpls=all_smpls) dml_obj.fit() np.random.seed(3141) - res_manual = fit_apo(y, x, d, - clone(learner[0]), clone(learner[1]), - treatment_level, - all_smpls, score, - normalize_ipw=normalize_ipw, trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_apo( + y, + x, + d, + clone(learner[0]), + clone(learner[1]), + treatment_level, + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_apo(y, d, treatment_level, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_apo( + y, + d, + treatment_level, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_apo_coef(dml_apo_classifier_fixture): - assert math.isclose(dml_apo_classifier_fixture['coef'], - dml_apo_classifier_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_apo_classifier_fixture["coef"], dml_apo_classifier_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_apo_se(dml_apo_classifier_fixture): - assert math.isclose(dml_apo_classifier_fixture['se'], - dml_apo_classifier_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_classifier_fixture["se"], dml_apo_classifier_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_apo_boot(dml_apo_classifier_fixture): - for bootstrap in dml_apo_classifier_fixture['boot_methods']: - assert np.allclose(dml_apo_classifier_fixture['boot_t_stat' + bootstrap], - dml_apo_classifier_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apo_classifier_fixture["boot_methods"]: + assert np.allclose( + dml_apo_classifier_fixture["boot_t_stat" + bootstrap], + dml_apo_classifier_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_apo_exceptions.py b/doubleml/irm/tests/test_apo_exceptions.py index 937f8affc..7692177e4 100644 --- a/doubleml/irm/tests/test_apo_exceptions.py +++ b/doubleml/irm/tests/test_apo_exceptions.py @@ -9,10 +9,12 @@ n = 100 data_apo = make_irm_data_discrete_treatments(n_obs=n) -df_apo = pd.DataFrame(np.column_stack((data_apo['y'], data_apo['d'], data_apo['x'])), - columns=['y', 'd'] + ['x' + str(i) for i in range(data_apo['x'].shape[1])]) +df_apo = pd.DataFrame( + np.column_stack((data_apo["y"], data_apo["d"], data_apo["x"])), + columns=["y", "d"] + ["x" + str(i) for i in range(data_apo["x"].shape[1])], +) -dml_data = DoubleMLData(df_apo, 'y', 'd') +dml_data = DoubleMLData(df_apo, "y", "d") ml_g = Lasso() ml_m = LogisticRegression() @@ -20,42 +22,44 @@ @pytest.mark.ci def test_apo_exception_data(): - msg = 'The data must be of DoubleMLData or DoubleMLClusterData type.' + msg = "The data must be of DoubleMLData or DoubleMLClusterData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLAPO(pd.DataFrame(), ml_g, ml_m, treatment_level=0) - msg = 'Only one treatment variable is allowed. Got 2 treatment variables.' + msg = "Only one treatment variable is allowed. Got 2 treatment variables." with pytest.raises(ValueError, match=msg): - dml_data_multiple = DoubleMLData(df_apo, 'y', ['d', 'x1']) + dml_data_multiple = DoubleMLData(df_apo, "y", ["d", "x1"]) _ = DoubleMLAPO(dml_data_multiple, ml_g, ml_m, treatment_level=0) dml_data_z = make_iivm_data() - msg = r'Incompatible data. z have been set as instrumental variable\(s\).' + msg = r"Incompatible data. z have been set as instrumental variable\(s\)." with pytest.raises(ValueError, match=msg): _ = DoubleMLAPO(dml_data_z, ml_g, ml_m, treatment_level=0) - msg = 'The number of treated observations is less than 5. Number of treated observations: 0 for treatment level 1.1.' + msg = "The number of treated observations is less than 5. Number of treated observations: 0 for treatment level 1.1." with pytest.raises(ValueError, match=msg): _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=1.1) - msg = r'The proportion of observations with treatment level 42 is less than 5\%. Got 0.70\%.' + msg = r"The proportion of observations with treatment level 42 is less than 5\%. Got 0.70\%." # test warning with pytest.warns(UserWarning, match=msg): data_apo_warn = make_irm_data_discrete_treatments(n_obs=1000) - data_apo_warn['d'][0:7] = 42 + data_apo_warn["d"][0:7] = 42 df_apo_warn = pd.DataFrame( - np.column_stack((data_apo_warn['y'], data_apo_warn['d'], data_apo_warn['x'])), - columns=['y', 'd'] + ['x' + str(i) for i in range(data_apo_warn['x'].shape[1])] + np.column_stack((data_apo_warn["y"], data_apo_warn["d"], data_apo_warn["x"])), + columns=["y", "d"] + ["x" + str(i) for i in range(data_apo_warn["x"].shape[1])], ) - dml_data_warn = DoubleMLData(df_apo_warn, 'y', 'd') + dml_data_warn = DoubleMLData(df_apo_warn, "y", "d") _ = DoubleMLAPO(dml_data_warn, ml_g, ml_m, treatment_level=42) @pytest.mark.ci def test_apo_exception_learner(): - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not' - ' binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not" + " binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): ml_g_classifier = LogisticRegression() _ = DoubleMLAPO(dml_data, ml_g_classifier, ml_m, treatment_level=0) @@ -63,27 +67,25 @@ def test_apo_exception_learner(): @pytest.mark.ci def test_apo_exception_scores(): - msg = 'Invalid score MAR. Valid score APO.' + msg = "Invalid score MAR. Valid score APO." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, score='MAR') + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, score="MAR") @pytest.mark.ci def test_apo_exception_trimming_rule(): - msg = 'Invalid trimming_rule discard. Valid trimming_rule truncate.' + msg = "Invalid trimming_rule discard. Valid trimming_rule truncate." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, trimming_rule='discard') + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, trimming_rule="discard") # check the trimming_threshold exceptions msg = "trimming_threshold has to be a float. Object of type passed." with pytest.raises(TypeError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - trimming_rule='truncate', trimming_threshold="0.1") + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, trimming_rule="truncate", trimming_threshold="0.1") - msg = 'Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5.' + msg = "Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - trimming_rule='truncate', trimming_threshold=0.6) + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, trimming_rule="truncate", trimming_threshold=0.6) @pytest.mark.ci @@ -100,7 +102,7 @@ def test_apo_exception_weights(): _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, weights=1) msg = r"weights must have keys \['weights', 'weights_bar'\]. keys dict_keys\(\['d'\]\) were passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, weights={'d': [1, 2, 3]}) + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, weights={"d": [1, 2, 3]}) # shape checks msg = rf"weights must have shape \({n},\). weights of shape \(1,\) was passed." @@ -112,41 +114,78 @@ def test_apo_exception_weights(): msg = rf"weights must have shape \({n},\). weights of shape \(1,\) was passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': np.ones(1), 'weights_bar': np.ones(1)}) + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, weights={"weights": np.ones(1), "weights_bar": np.ones(1)}) msg = rf"weights must have shape \({n},\). weights of shape \({n}, 2\) was passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': np.ones((n, 2)), 'weights_bar': np.ones((n, 2))}) + _ = DoubleMLAPO( + dml_data, ml_g, ml_m, treatment_level=0, weights={"weights": np.ones((n, 2)), "weights_bar": np.ones((n, 2))} + ) msg = rf"weights_bar must have shape \({n}, 1\). weights_bar of shape \({n}, 2\) was passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': np.ones(n), 'weights_bar': np.ones((n, 2))}) + _ = DoubleMLAPO( + dml_data, ml_g, ml_m, treatment_level=0, weights={"weights": np.ones(n), "weights_bar": np.ones((n, 2))} + ) # value checks msg = "All weights values must be greater or equal 0." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights=-1*np.ones(n,)) + _ = DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=0, + weights=-1 + * np.ones( + n, + ), + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': -1*np.ones(n,), 'weights_bar': np.ones((n, 1))}) + _ = DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=0, + weights={ + "weights": -1 + * np.ones( + n, + ), + "weights_bar": np.ones((n, 1)), + }, + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': np.ones(n,), 'weights_bar': -1*np.ones((n, 1))}) + _ = DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=0, + weights={ + "weights": np.ones( + n, + ), + "weights_bar": -1 * np.ones((n, 1)), + }, + ) msg = "At least one weight must be non-zero." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights=np.zeros((dml_data.d.shape[0], ))) + _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, weights=np.zeros((dml_data.d.shape[0],))) with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': np.zeros((dml_data.d.shape[0], )), - 'weights_bar': np.ones((dml_data.d.shape[0], 1))}) + _ = DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=0, + weights={"weights": np.zeros((dml_data.d.shape[0],)), "weights_bar": np.ones((dml_data.d.shape[0], 1))}, + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPO(dml_data, ml_g, ml_m, treatment_level=0, - weights={'weights': np.ones((dml_data.d.shape[0], )), - 'weights_bar': np.zeros((dml_data.d.shape[0], 1))}) + _ = DoubleMLAPO( + dml_data, + ml_g, + ml_m, + treatment_level=0, + weights={"weights": np.ones((dml_data.d.shape[0],)), "weights_bar": np.zeros((dml_data.d.shape[0], 1))}, + ) @pytest.mark.ci @@ -160,10 +199,7 @@ def test_apo_exception_capo_gapo(): ml_g = RandomForestRegressor(n_estimators=10) ml_m = RandomForestClassifier(n_estimators=10) - dml_obj = DoubleMLAPO(obj_dml_data, - ml_m=ml_m, - ml_g=ml_g, - treatment_level=0) + dml_obj = DoubleMLAPO(obj_dml_data, ml_m=ml_m, ml_g=ml_g, treatment_level=0) dml_obj.fit() # create a random basis @@ -171,10 +207,10 @@ def test_apo_exception_capo_gapo(): msg = "Invalid score APO_2. Valid score APO." with pytest.raises(ValueError, match=msg): - dml_obj._score = 'APO_2' + dml_obj._score = "APO_2" _ = dml_obj.capo(random_basis) # reset the score - dml_obj._score = 'APO' + dml_obj._score = "APO" msg = "Only implemented for one repetition. Number of repetitions is 2." with pytest.raises(NotImplementedError, match=msg): @@ -188,10 +224,11 @@ def test_apo_exception_capo_gapo(): _ = dml_obj.gapo(1) groups_1 = pd.DataFrame( - np.column_stack([obj_dml_data.data['X1'] > 0.2, np.ones_like(obj_dml_data.data['X1'])]), - columns=['Group 1', 'Group 2'] + np.column_stack([obj_dml_data.data["X1"] > 0.2, np.ones_like(obj_dml_data.data["X1"])]), columns=["Group 1", "Group 2"] + ) + msg = ( + r"Columns of groups must be of bool type or int type \(dummy coded\). Alternatively," + " groups should only contain one column." ) - msg = (r'Columns of groups must be of bool type or int type \(dummy coded\). Alternatively,' - ' groups should only contain one column.') with pytest.raises(TypeError, match=msg): _ = dml_obj.gapo(groups_1) diff --git a/doubleml/irm/tests/test_apo_external_predictions.py b/doubleml/irm/tests/test_apo_external_predictions.py index 6c7900788..b2cc5d6c8 100644 --- a/doubleml/irm/tests/test_apo_external_predictions.py +++ b/doubleml/irm/tests/test_apo_external_predictions.py @@ -38,12 +38,12 @@ def doubleml_apo_ext_fixture(n_rep, set_ml_m_ext, set_ml_g_ext): n_obs = 500 data_apo = make_irm_data_discrete_treatments(n_obs=n_obs) df_apo = pd.DataFrame( - np.column_stack((data_apo['y'], data_apo['d'], data_apo['x'])), - columns=['y', 'd'] + ['x' + str(i) for i in range(data_apo['x'].shape[1])] + np.column_stack((data_apo["y"], data_apo["d"], data_apo["x"])), + columns=["y", "d"] + ["x" + str(i) for i in range(data_apo["x"].shape[1])], ) - dml_data = DoubleMLData(df_apo, 'y', 'd') - d = data_apo['d'] + dml_data = DoubleMLData(df_apo, "y", "d") + d = data_apo["d"] all_smpls = draw_smpls(n_obs, n_folds=5, n_rep=n_rep, groups=d) kwargs = { @@ -51,7 +51,7 @@ def doubleml_apo_ext_fixture(n_rep, set_ml_m_ext, set_ml_g_ext): "score": score, "treatment_level": treatment_level, "n_rep": n_rep, - "draw_sample_splitting": False + "draw_sample_splitting": False, } dml_obj = DoubleMLAPO(ml_g=LinearRegression(), ml_m=LogisticRegression(), **kwargs) @@ -79,10 +79,7 @@ def doubleml_apo_ext_fixture(n_rep, set_ml_m_ext, set_ml_g_ext): np.random.seed(3141) dml_obj_ext.fit(external_predictions=ext_predictions) - res_dict = { - "coef_normal": dml_obj.coef[0], - "coef_ext": dml_obj_ext.coef[0] - } + res_dict = {"coef_normal": dml_obj.coef[0], "coef_ext": dml_obj_ext.coef[0]} return res_dict @@ -90,8 +87,5 @@ def doubleml_apo_ext_fixture(n_rep, set_ml_m_ext, set_ml_g_ext): @pytest.mark.ci def test_doubleml_apo_ext_coef(doubleml_apo_ext_fixture): assert math.isclose( - doubleml_apo_ext_fixture["coef_normal"], - doubleml_apo_ext_fixture["coef_ext"], - rel_tol=1e-9, - abs_tol=1e-4 + doubleml_apo_ext_fixture["coef_normal"], doubleml_apo_ext_fixture["coef_ext"], rel_tol=1e-9, abs_tol=1e-4 ) diff --git a/doubleml/irm/tests/test_apo_tune.py b/doubleml/irm/tests/test_apo_tune.py index 4450325ea..b7081618c 100644 --- a/doubleml/irm/tests/test_apo_tune.py +++ b/doubleml/irm/tests/test_apo_tune.py @@ -12,52 +12,46 @@ from ._utils_apo_manual import boot_apo, fit_apo, tune_nuisance_apo -@pytest.fixture(scope='module', - params=[RandomForestRegressor(random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression(random_state=42)]) +@pytest.fixture(scope="module", params=[LogisticRegression(random_state=42)]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=['APO']) +@pytest.fixture(scope="module", params=["APO"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ in [LogisticRegression] - par_grid = {'C': np.logspace(-4, 2, 10)} + par_grid = {"C": np.logspace(-4, 2, 10)} return par_grid -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_apo_tune_fixture(generate_data_irm, learner_g, learner_m, score, normalize_ipw, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 treatment_level = 0 @@ -73,19 +67,21 @@ def dml_apo_tune_fixture(generate_data_irm, learner_g, learner_m, score, normali np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) - dml_obj = dml.DoubleMLAPO(obj_dml_data, - ml_g, ml_m, - treatment_level=treatment_level, - n_folds=n_folds, - score=score, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) + dml_obj = dml.DoubleMLAPO( + obj_dml_data, + ml_g, + ml_m, + treatment_level=treatment_level, + n_folds=n_folds, + score=score, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_obj.set_sample_splitting(all_smpls=all_smpls) np.random.seed(3141) # tune hyperparameters - tune_res = dml_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLAPO) dml_obj.fit() @@ -94,65 +90,103 @@ def dml_apo_tune_fixture(generate_data_irm, learner_g, learner_m, score, normali smpls = all_smpls[0] if tune_on_folds: - g0_params, g1_params, m_params = tune_nuisance_apo(y, x, d, treatment_level, - clone(learner_g), clone(learner_m), smpls, score, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g0_params, g1_params, m_params = tune_nuisance_apo( + y, + x, + d, + treatment_level, + clone(learner_g), + clone(learner_m), + smpls, + score, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + ) else: xx = [(np.arange(len(y)), np.array([]))] - g0_params, g1_params, m_params = tune_nuisance_apo(y, x, d, treatment_level, - clone(learner_g), clone(learner_m), xx, score, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g0_params, g1_params, m_params = tune_nuisance_apo( + y, + x, + d, + treatment_level, + clone(learner_g), + clone(learner_m), + xx, + score, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + ) g0_params = g0_params * n_folds m_params = m_params * n_folds g1_params = g1_params * n_folds - res_manual = fit_apo(y, x, d, clone(learner_g), clone(learner_m), - treatment_level, - all_smpls, score, - normalize_ipw=normalize_ipw, - g0_params=g0_params, g1_params=g1_params, m_params=m_params) - - res_dict = {'coef': dml_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_apo( + y, + x, + d, + clone(learner_g), + clone(learner_m), + treatment_level, + all_smpls, + score, + normalize_ipw=normalize_ipw, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + ) + + res_dict = { + "coef": dml_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_apo(y, d, treatment_level, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_apo( + y, + d, + treatment_level, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_apo_tune_coef(dml_apo_tune_fixture): - assert math.isclose(dml_apo_tune_fixture['coef'], - dml_apo_tune_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_tune_fixture["coef"], dml_apo_tune_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_apo_tune_se(dml_apo_tune_fixture): - assert math.isclose(dml_apo_tune_fixture['se'], - dml_apo_tune_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_apo_tune_fixture["se"], dml_apo_tune_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_apo_tune_boot(dml_apo_tune_fixture): - for bootstrap in dml_apo_tune_fixture['boot_methods']: - assert np.allclose(dml_apo_tune_fixture['boot_t_stat' + bootstrap], - dml_apo_tune_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apo_tune_fixture["boot_methods"]: + assert np.allclose( + dml_apo_tune_fixture["boot_t_stat" + bootstrap], + dml_apo_tune_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_apo_weighted_scores.py b/doubleml/irm/tests/test_apo_weighted_scores.py index 73733c8f9..63687ebdc 100644 --- a/doubleml/irm/tests/test_apo_weighted_scores.py +++ b/doubleml/irm/tests/test_apo_weighted_scores.py @@ -9,48 +9,47 @@ from ...tests._utils import draw_smpls -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['APO']) +@pytest.fixture(scope="module", params=["APO"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment_level(request): return request.param -@pytest.fixture(scope='module') -def weighted_apo_score_fixture(generate_data_irm, learner, score, n_rep, normalize_ipw, trimming_threshold, - treatment_level): +@pytest.fixture(scope="module") +def weighted_apo_score_fixture(generate_data_irm, learner, score, n_rep, normalize_ipw, trimming_threshold, treatment_level): n_folds = 2 # collect data @@ -85,36 +84,36 @@ def weighted_apo_score_fixture(generate_data_irm, learner, score, n_rep, normali np.random.seed(42) weights_dict = { - 'weights': weights, - 'weights_bar': np.tile(weights[:, np.newaxis], (1, n_rep)), + "weights": weights, + "weights_bar": np.tile(weights[:, np.newaxis], (1, n_rep)), } dml_obj_weighted_dict = dml.DoubleMLAPO(weights=weights_dict, **input_args) dml_obj_weighted_dict.set_sample_splitting(all_smpls=all_smpls) dml_obj_weighted_dict.fit() result_dict = { - 'coef': dml_obj.coef, - 'weighted_coef': dml_obj_weighted.coef, - 'weighted_coef_dict': dml_obj_weighted_dict.coef, - 'default_weights': dml_obj.weights, + "coef": dml_obj.coef, + "weighted_coef": dml_obj_weighted.coef, + "weighted_coef_dict": dml_obj_weighted_dict.coef, + "default_weights": dml_obj.weights, } return result_dict @pytest.mark.ci def test_apo_weighted_coef(weighted_apo_score_fixture): - assert np.allclose(0.5 * weighted_apo_score_fixture['coef'], - weighted_apo_score_fixture['weighted_coef']) - assert np.allclose(0.5 * weighted_apo_score_fixture['coef'], - weighted_apo_score_fixture['weighted_coef_dict']) + assert np.allclose(0.5 * weighted_apo_score_fixture["coef"], weighted_apo_score_fixture["weighted_coef"]) + assert np.allclose(0.5 * weighted_apo_score_fixture["coef"], weighted_apo_score_fixture["weighted_coef_dict"]) @pytest.mark.ci def test_apo_default_weights(weighted_apo_score_fixture): - assert isinstance(weighted_apo_score_fixture['default_weights'], dict) + assert isinstance(weighted_apo_score_fixture["default_weights"], dict) - expected_keys = {'weights'} - assert set(weighted_apo_score_fixture['default_weights'].keys()) == expected_keys + expected_keys = {"weights"} + assert set(weighted_apo_score_fixture["default_weights"].keys()) == expected_keys - assert np.allclose(weighted_apo_score_fixture['default_weights']['weights'], - np.ones_like(weighted_apo_score_fixture['default_weights']['weights'])) + assert np.allclose( + weighted_apo_score_fixture["default_weights"]["weights"], + np.ones_like(weighted_apo_score_fixture["default_weights"]["weights"]), + ) diff --git a/doubleml/irm/tests/test_apos.py b/doubleml/irm/tests/test_apos.py index f4e6af54c..746cb63c9 100644 --- a/doubleml/irm/tests/test_apos.py +++ b/doubleml/irm/tests/test_apos.py @@ -19,10 +19,12 @@ def test_apo_properties(): np.random.seed(42) obj_dml_data = make_irm_data(n_obs=n, dim_x=2) - dml_obj = dml.DoubleMLAPOS(obj_dml_data, - ml_g=RandomForestRegressor(n_estimators=10), - ml_m=RandomForestClassifier(n_estimators=10), - treatment_levels=0) + dml_obj = dml.DoubleMLAPOS( + obj_dml_data, + ml_g=RandomForestRegressor(n_estimators=10), + ml_m=RandomForestClassifier(n_estimators=10), + treatment_levels=0, + ) # check properties before fit assert dml_obj.n_rep_boot is None @@ -63,70 +65,68 @@ def test_apo_properties(): assert dml_obj.sensitivity_params is not None -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250, random_state=42)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250, random_state=42)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[1, 5]) +@pytest.fixture(scope="module", params=[1, 5]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=[[0, 1, 2], [0]]) +@pytest.fixture(scope="module", params=[[0, 1, 2], [0]]) def treatment_levels(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_apos_fixture(learner, n_rep, normalize_ipw, trimming_threshold, treatment_levels): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 np.random.seed(3141) n_obs = 500 data = make_irm_data_discrete_treatments(n_obs=n_obs) - y = data['y'] - x = data['x'] - d = data['d'] - df = pd.DataFrame( - np.column_stack((y, d, x)), - columns=['y', 'd'] + ['x' + str(i) for i in range(data['x'].shape[1])] - ) + y = data["y"] + x = data["x"] + d = data["d"] + df = pd.DataFrame(np.column_stack((y, d, x)), columns=["y", "d"] + ["x" + str(i) for i in range(data["x"].shape[1])]) - dml_data = dml.DoubleMLData(df, 'y', 'd') + dml_data = dml.DoubleMLData(df, "y", "d") input_args = { - 'obj_dml_data': dml_data, - 'ml_g': clone(learner[0]), - 'ml_m': clone(learner[1]), + "obj_dml_data": dml_data, + "ml_g": clone(learner[0]), + "ml_m": clone(learner[1]), "treatment_levels": treatment_levels, "n_folds": n_folds, "n_rep": n_rep, - "score": 'APO', + "score": "APO", "normalize_ipw": normalize_ipw, - "trimming_rule": 'truncate', + "trimming_rule": "truncate", "trimming_threshold": trimming_threshold, - } + } unfitted_apos_model = dml.DoubleMLAPOS(**input_args) np.random.seed(42) @@ -142,147 +142,138 @@ def dml_apos_fixture(learner, n_rep, normalize_ipw, trimming_threshold, treatmen np.random.seed(42) res_manual = fit_apos( - y, x, d, - clone(learner[0]), clone(learner[1]), + y, + x, + d, + clone(learner[0]), + clone(learner[1]), treatment_levels=treatment_levels, all_smpls=all_smpls, n_rep=n_rep, - score='APO', - trimming_rule='truncate', + score="APO", + trimming_rule="truncate", normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + trimming_threshold=trimming_threshold, + ) ci = dml_obj.confint(joint=False, level=0.95) ci_ext_smpls = dml_obj_ext_smpls.confint(joint=False, level=0.95) ci_manual = confint_manual( - res_manual['apos'], res_manual['se'], treatment_levels, - boot_t_stat=None, joint=False, level=0.95 - ) + res_manual["apos"], res_manual["se"], treatment_levels, boot_t_stat=None, joint=False, level=0.95 + ) res_dict = { - 'coef': dml_obj.coef, - 'coef_ext_smpls': dml_obj_ext_smpls.coef, - 'coef_manual': res_manual['apos'], - 'se': dml_obj.se, - 'se_ext_smpls': dml_obj_ext_smpls.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods, - 'n_treatment_levels': len(treatment_levels), - 'n_rep': n_rep, - 'ci': ci.to_numpy(), - 'ci_ext_smpls': ci_ext_smpls.to_numpy(), - 'ci_manual': ci_manual.to_numpy(), - 'apos_model': dml_obj, - 'unfitted_apos_model': unfitted_apos_model + "coef": dml_obj.coef, + "coef_ext_smpls": dml_obj_ext_smpls.coef, + "coef_manual": res_manual["apos"], + "se": dml_obj.se, + "se_ext_smpls": dml_obj_ext_smpls.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + "n_treatment_levels": len(treatment_levels), + "n_rep": n_rep, + "ci": ci.to_numpy(), + "ci_ext_smpls": ci_ext_smpls.to_numpy(), + "ci_manual": ci_manual.to_numpy(), + "apos_model": dml_obj, + "unfitted_apos_model": unfitted_apos_model, } if n_rep == 1: for bootstrap in boot_methods: np.random.seed(42) - boot_t_stat = boot_apos(res_manual['apo_scaled_score'], res_manual['all_se'], treatment_levels, - all_smpls, n_rep, bootstrap, n_rep_boot) + boot_t_stat = boot_apos( + res_manual["apo_scaled_score"], res_manual["all_se"], treatment_levels, all_smpls, n_rep, bootstrap, n_rep_boot + ) np.random.seed(42) dml_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat_' + bootstrap] = dml_obj.boot_t_stat - res_dict['boot_t_stat_' + bootstrap + '_manual'] = boot_t_stat + res_dict["boot_t_stat_" + bootstrap] = dml_obj.boot_t_stat + res_dict["boot_t_stat_" + bootstrap + "_manual"] = boot_t_stat ci = dml_obj.confint(joint=True, level=0.95) ci_manual = confint_manual( - res_manual['apos'], res_manual['se'], treatment_levels, - boot_t_stat=boot_t_stat, joint=True, level=0.95) - res_dict['boot_ci_' + bootstrap] = ci.to_numpy() - res_dict['boot_ci_' + bootstrap + '_manual'] = ci_manual.to_numpy() + res_manual["apos"], res_manual["se"], treatment_levels, boot_t_stat=boot_t_stat, joint=True, level=0.95 + ) + res_dict["boot_ci_" + bootstrap] = ci.to_numpy() + res_dict["boot_ci_" + bootstrap + "_manual"] = ci_manual.to_numpy() # causal contrasts if len(treatment_levels) > 1: acc_single = dml_obj.causal_contrast(reference_levels=[treatment_levels[0]]) - res_dict['causal_contrast_single'] = acc_single + res_dict["causal_contrast_single"] = acc_single acc_multiple = dml_obj.causal_contrast(reference_levels=treatment_levels) - res_dict['causal_contrast_multiple'] = acc_multiple + res_dict["causal_contrast_multiple"] = acc_multiple return res_dict @pytest.mark.ci def test_dml_apos_coef(dml_apos_fixture): - assert np.allclose(dml_apos_fixture['coef'], - dml_apos_fixture['coef_manual'], - rtol=1e-9, atol=1e-9) - assert np.allclose(dml_apos_fixture['coef'], - dml_apos_fixture['coef_ext_smpls'], - rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_fixture["coef"], dml_apos_fixture["coef_manual"], rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_fixture["coef"], dml_apos_fixture["coef_ext_smpls"], rtol=1e-9, atol=1e-9) @pytest.mark.ci def test_dml_apos_se(dml_apos_fixture): - if dml_apos_fixture['n_rep'] != 1: + if dml_apos_fixture["n_rep"] != 1: pytest.skip("Skipping test as n_rep is not 1") - assert np.allclose(dml_apos_fixture['se'], - dml_apos_fixture['se_manual'], - rtol=1e-9, atol=1e-9) - assert np.allclose(dml_apos_fixture['se'], - dml_apos_fixture['se_ext_smpls'], - rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_fixture["se"], dml_apos_fixture["se_manual"], rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_fixture["se"], dml_apos_fixture["se_ext_smpls"], rtol=1e-9, atol=1e-9) @pytest.mark.ci def test_dml_apos_boot(dml_apos_fixture): - if dml_apos_fixture['n_rep'] != 1: + if dml_apos_fixture["n_rep"] != 1: pytest.skip("Skipping test as n_rep is not 1") - for bootstrap in dml_apos_fixture['boot_methods']: - assert np.allclose(dml_apos_fixture['boot_t_stat_' + bootstrap], - dml_apos_fixture['boot_t_stat_' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apos_fixture["boot_methods"]: + assert np.allclose( + dml_apos_fixture["boot_t_stat_" + bootstrap], + dml_apos_fixture["boot_t_stat_" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_apos_ci(dml_apos_fixture): - if dml_apos_fixture['n_rep'] != 1: + if dml_apos_fixture["n_rep"] != 1: pytest.skip("Skipping test as n_rep is not 1") - for bootstrap in dml_apos_fixture['boot_methods']: - assert np.allclose(dml_apos_fixture['ci'], - dml_apos_fixture['ci_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_apos_fixture['ci'], - dml_apos_fixture['ci_ext_smpls'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_apos_fixture['boot_ci_' + bootstrap], - dml_apos_fixture['boot_ci_' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apos_fixture["boot_methods"]: + assert np.allclose(dml_apos_fixture["ci"], dml_apos_fixture["ci_manual"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_apos_fixture["ci"], dml_apos_fixture["ci_ext_smpls"], rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_apos_fixture["boot_ci_" + bootstrap], + dml_apos_fixture["boot_ci_" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_doubleml_apos_return_types(dml_apos_fixture): - assert isinstance(dml_apos_fixture['apos_model'].__str__(), str) - assert isinstance(dml_apos_fixture['apos_model'].summary, pd.DataFrame) + assert isinstance(dml_apos_fixture["apos_model"].__str__(), str) + assert isinstance(dml_apos_fixture["apos_model"].summary, pd.DataFrame) - assert dml_apos_fixture['apos_model'].all_coef.shape == ( - dml_apos_fixture['n_treatment_levels'], - dml_apos_fixture['n_rep'] - ) - assert isinstance(dml_apos_fixture['unfitted_apos_model'].summary, pd.DataFrame) - if dml_apos_fixture['n_treatment_levels'] > 1: - assert isinstance(dml_apos_fixture['causal_contrast_single'], dml.DoubleMLFramework) - assert isinstance(dml_apos_fixture['causal_contrast_multiple'], dml.DoubleMLFramework) + assert dml_apos_fixture["apos_model"].all_coef.shape == (dml_apos_fixture["n_treatment_levels"], dml_apos_fixture["n_rep"]) + assert isinstance(dml_apos_fixture["unfitted_apos_model"].summary, pd.DataFrame) + if dml_apos_fixture["n_treatment_levels"] > 1: + assert isinstance(dml_apos_fixture["causal_contrast_single"], dml.DoubleMLFramework) + assert isinstance(dml_apos_fixture["causal_contrast_multiple"], dml.DoubleMLFramework) - benchmark = dml_apos_fixture['apos_model'].sensitivity_benchmark(benchmarking_set=['x1']) + benchmark = dml_apos_fixture["apos_model"].sensitivity_benchmark(benchmarking_set=["x1"]) assert isinstance(benchmark, pd.DataFrame) @pytest.mark.ci def test_doubleml_apos_causal_contrast(dml_apos_fixture): - if dml_apos_fixture['n_treatment_levels'] == 1: + if dml_apos_fixture["n_treatment_levels"] == 1: pytest.skip("Skipping test as n_treatment_levels is 1") - acc_single = dml_apos_fixture['apos_model'].all_coef[1:, ] - dml_apos_fixture['apos_model'].all_coef[0, ] - assert np.allclose(dml_apos_fixture['causal_contrast_single'].all_thetas, - acc_single, - rtol=1e-9, atol=1e-9) - - acc_multiple = np.append(acc_single, - dml_apos_fixture['apos_model'].all_coef[2:3, ] - dml_apos_fixture['apos_model'].all_coef[1:2, ], - axis=0) - assert np.allclose(dml_apos_fixture['causal_contrast_multiple'].all_thetas, - acc_multiple, - rtol=1e-9, atol=1e-9) + acc_single = dml_apos_fixture["apos_model"].all_coef[1:,] - dml_apos_fixture["apos_model"].all_coef[0,] + assert np.allclose(dml_apos_fixture["causal_contrast_single"].all_thetas, acc_single, rtol=1e-9, atol=1e-9) + + acc_multiple = np.append( + acc_single, dml_apos_fixture["apos_model"].all_coef[2:3,] - dml_apos_fixture["apos_model"].all_coef[1:2,], axis=0 + ) + assert np.allclose(dml_apos_fixture["causal_contrast_multiple"].all_thetas, acc_multiple, rtol=1e-9, atol=1e-9) diff --git a/doubleml/irm/tests/test_apos_classfier.py b/doubleml/irm/tests/test_apos_classfier.py index b6c17fcb8..06fdc3085 100644 --- a/doubleml/irm/tests/test_apos_classfier.py +++ b/doubleml/irm/tests/test_apos_classfier.py @@ -12,42 +12,43 @@ from ._utils_apos_manual import boot_apos, fit_apos -@pytest.fixture(scope='module', - params=[[LogisticRegression(solver='lbfgs', max_iter=250), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LogisticRegression(solver="lbfgs", max_iter=250), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[1]) +@pytest.fixture(scope="module", params=[1]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=[[0, 1, 2], [0]]) +@pytest.fixture(scope="module", params=[[0, 1, 2], [0]]) def treatment_levels(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_apos_classifier_fixture(learner, n_rep, normalize_ipw, trimming_threshold, treatment_levels): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -55,27 +56,24 @@ def dml_apos_classifier_fixture(learner, n_rep, normalize_ipw, trimming_threshol n_obs = 500 data = make_irm_data_discrete_treatments(n_obs=n_obs) y = np.random.binomial(1, 0.5, n_obs) - x = data['x'] - d = data['d'] - df = pd.DataFrame( - np.column_stack((y, d, x)), - columns=['y', 'd'] + ['x' + str(i) for i in range(data['x'].shape[1])] - ) + x = data["x"] + d = data["d"] + df = pd.DataFrame(np.column_stack((y, d, x)), columns=["y", "d"] + ["x" + str(i) for i in range(data["x"].shape[1])]) - dml_data = dml.DoubleMLData(df, 'y', 'd') + dml_data = dml.DoubleMLData(df, "y", "d") input_args = { - 'obj_dml_data': dml_data, - 'ml_g': clone(learner[0]), - 'ml_m': clone(learner[1]), + "obj_dml_data": dml_data, + "ml_g": clone(learner[0]), + "ml_m": clone(learner[1]), "treatment_levels": treatment_levels, "n_folds": n_folds, "n_rep": n_rep, - "score": 'APO', + "score": "APO", "normalize_ipw": normalize_ipw, - "trimming_rule": 'truncate', + "trimming_rule": "truncate", "trimming_threshold": trimming_threshold, - } + } unfitted_apos_model = dml.DoubleMLAPOS(**input_args) np.random.seed(42) @@ -91,108 +89,111 @@ def dml_apos_classifier_fixture(learner, n_rep, normalize_ipw, trimming_threshol np.random.seed(42) res_manual = fit_apos( - y, x, d, - clone(learner[0]), clone(learner[1]), + y, + x, + d, + clone(learner[0]), + clone(learner[1]), treatment_levels=treatment_levels, all_smpls=all_smpls, - score='APO', - trimming_rule='truncate', + score="APO", + trimming_rule="truncate", normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + trimming_threshold=trimming_threshold, + ) ci = dml_obj.confint(joint=False, level=0.95) ci_ext_smpls = dml_obj_ext_smpls.confint(joint=False, level=0.95) ci_manual = confint_manual( - res_manual['apos'], res_manual['se'], treatment_levels, - boot_t_stat=None, joint=False, level=0.95 - ) + res_manual["apos"], res_manual["se"], treatment_levels, boot_t_stat=None, joint=False, level=0.95 + ) res_dict = { - 'coef': dml_obj.coef, - 'coef_ext_smpls': dml_obj_ext_smpls.coef, - 'coef_manual': res_manual['apos'], - 'se': dml_obj.se, - 'se_ext_smpls': dml_obj_ext_smpls.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods, - 'n_treatment_levels': len(treatment_levels), - 'n_rep': n_rep, - 'ci': ci.to_numpy(), - 'ci_ext_smpls': ci_ext_smpls.to_numpy(), - 'ci_manual': ci_manual.to_numpy(), - 'apos_model': dml_obj, - 'unfitted_apos_model': unfitted_apos_model + "coef": dml_obj.coef, + "coef_ext_smpls": dml_obj_ext_smpls.coef, + "coef_manual": res_manual["apos"], + "se": dml_obj.se, + "se_ext_smpls": dml_obj_ext_smpls.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + "n_treatment_levels": len(treatment_levels), + "n_rep": n_rep, + "ci": ci.to_numpy(), + "ci_ext_smpls": ci_ext_smpls.to_numpy(), + "ci_manual": ci_manual.to_numpy(), + "apos_model": dml_obj, + "unfitted_apos_model": unfitted_apos_model, } for bootstrap in boot_methods: np.random.seed(42) - boot_t_stat = boot_apos(res_manual['apo_scaled_score'], res_manual['all_se'], treatment_levels, - all_smpls, n_rep, bootstrap, n_rep_boot) + boot_t_stat = boot_apos( + res_manual["apo_scaled_score"], res_manual["all_se"], treatment_levels, all_smpls, n_rep, bootstrap, n_rep_boot + ) np.random.seed(42) dml_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat_' + bootstrap] = dml_obj.boot_t_stat - res_dict['boot_t_stat_' + bootstrap + '_manual'] = boot_t_stat + res_dict["boot_t_stat_" + bootstrap] = dml_obj.boot_t_stat + res_dict["boot_t_stat_" + bootstrap + "_manual"] = boot_t_stat ci = dml_obj.confint(joint=True, level=0.95) - ci_manual = confint_manual(res_manual['apos'], res_manual['se'], treatment_levels, - boot_t_stat=boot_t_stat, joint=True, level=0.95) - res_dict['boot_ci_' + bootstrap] = ci.to_numpy() - res_dict['boot_ci_' + bootstrap + '_manual'] = ci_manual.to_numpy() + ci_manual = confint_manual( + res_manual["apos"], res_manual["se"], treatment_levels, boot_t_stat=boot_t_stat, joint=True, level=0.95 + ) + res_dict["boot_ci_" + bootstrap] = ci.to_numpy() + res_dict["boot_ci_" + bootstrap + "_manual"] = ci_manual.to_numpy() return res_dict @pytest.mark.ci def test_dml_apos_coef(dml_apos_classifier_fixture): - assert np.allclose(dml_apos_classifier_fixture['coef'], - dml_apos_classifier_fixture['coef_manual'], - rtol=1e-9, atol=1e-9) - assert np.allclose(dml_apos_classifier_fixture['coef'], - dml_apos_classifier_fixture['coef_ext_smpls'], - rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_classifier_fixture["coef"], dml_apos_classifier_fixture["coef_manual"], rtol=1e-9, atol=1e-9) + assert np.allclose( + dml_apos_classifier_fixture["coef"], dml_apos_classifier_fixture["coef_ext_smpls"], rtol=1e-9, atol=1e-9 + ) @pytest.mark.ci def test_dml_apos_se(dml_apos_classifier_fixture): - assert np.allclose(dml_apos_classifier_fixture['se'], - dml_apos_classifier_fixture['se_manual'], - rtol=1e-9, atol=1e-9) - assert np.allclose(dml_apos_classifier_fixture['se'], - dml_apos_classifier_fixture['se_ext_smpls'], - rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_classifier_fixture["se"], dml_apos_classifier_fixture["se_manual"], rtol=1e-9, atol=1e-9) + assert np.allclose(dml_apos_classifier_fixture["se"], dml_apos_classifier_fixture["se_ext_smpls"], rtol=1e-9, atol=1e-9) @pytest.mark.ci def test_dml_apos_boot(dml_apos_classifier_fixture): - for bootstrap in dml_apos_classifier_fixture['boot_methods']: - assert np.allclose(dml_apos_classifier_fixture['boot_t_stat_' + bootstrap], - dml_apos_classifier_fixture['boot_t_stat_' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apos_classifier_fixture["boot_methods"]: + assert np.allclose( + dml_apos_classifier_fixture["boot_t_stat_" + bootstrap], + dml_apos_classifier_fixture["boot_t_stat_" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_apos_ci(dml_apos_classifier_fixture): - for bootstrap in dml_apos_classifier_fixture['boot_methods']: - assert np.allclose(dml_apos_classifier_fixture['ci'], - dml_apos_classifier_fixture['ci_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_apos_classifier_fixture['ci'], - dml_apos_classifier_fixture['ci_ext_smpls'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_apos_classifier_fixture['boot_ci_' + bootstrap], - dml_apos_classifier_fixture['boot_ci_' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_apos_classifier_fixture["boot_methods"]: + assert np.allclose(dml_apos_classifier_fixture["ci"], dml_apos_classifier_fixture["ci_manual"], rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_apos_classifier_fixture["ci"], dml_apos_classifier_fixture["ci_ext_smpls"], rtol=1e-9, atol=1e-4 + ) + assert np.allclose( + dml_apos_classifier_fixture["boot_ci_" + bootstrap], + dml_apos_classifier_fixture["boot_ci_" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_doubleml_apos_return_types(dml_apos_classifier_fixture): - assert isinstance(dml_apos_classifier_fixture['apos_model'].__str__(), str) - assert isinstance(dml_apos_classifier_fixture['apos_model'].summary, pd.DataFrame) + assert isinstance(dml_apos_classifier_fixture["apos_model"].__str__(), str) + assert isinstance(dml_apos_classifier_fixture["apos_model"].summary, pd.DataFrame) - assert dml_apos_classifier_fixture['apos_model'].all_coef.shape == ( - dml_apos_classifier_fixture['n_treatment_levels'], - dml_apos_classifier_fixture['n_rep'] + assert dml_apos_classifier_fixture["apos_model"].all_coef.shape == ( + dml_apos_classifier_fixture["n_treatment_levels"], + dml_apos_classifier_fixture["n_rep"], ) - assert isinstance(dml_apos_classifier_fixture['unfitted_apos_model'].summary, pd.DataFrame) + assert isinstance(dml_apos_classifier_fixture["unfitted_apos_model"].summary, pd.DataFrame) diff --git a/doubleml/irm/tests/test_apos_exceptions.py b/doubleml/irm/tests/test_apos_exceptions.py index ca7e863ba..8e9a0b8a6 100644 --- a/doubleml/irm/tests/test_apos_exceptions.py +++ b/doubleml/irm/tests/test_apos_exceptions.py @@ -9,11 +9,10 @@ n = 100 data = make_irm_data_discrete_treatments(n_obs=n) df = pd.DataFrame( - np.column_stack((data['y'], data['d'], data['x'])), - columns=['y', 'd'] + ['x' + str(i) for i in range(data['x'].shape[1])] + np.column_stack((data["y"], data["d"], data["x"])), columns=["y", "d"] + ["x" + str(i) for i in range(data["x"].shape[1])] ) -dml_data = DoubleMLData(df, 'y', 'd') +dml_data = DoubleMLData(df, "y", "d") ml_g = Lasso() ml_m = LogisticRegression() @@ -21,17 +20,19 @@ @pytest.mark.ci def test_apos_exception_data(): - msg = 'The data must be of DoubleMLData or DoubleMLClusterData type.' + msg = "The data must be of DoubleMLData or DoubleMLClusterData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLAPOS(pd.DataFrame(), ml_g, ml_m, treatment_levels=0) - msg = 'The data must not contain instrumental variables.' + msg = "The data must not contain instrumental variables." with pytest.raises(ValueError, match=msg): dml_data_z = make_iivm_data() _ = DoubleMLAPOS(dml_data_z, ml_g, ml_m, treatment_levels=0) - msg = ('Invalid reference_levels. reference_levels has to be an iterable subset or ' - 'a single element of the unique treatment levels in the data.') + msg = ( + "Invalid reference_levels. reference_levels has to be an iterable subset or " + "a single element of the unique treatment levels in the data." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=[1.1]) with pytest.raises(ValueError, match=msg): @@ -42,8 +43,10 @@ def test_apos_exception_data(): @pytest.mark.ci def test_apos_exception_learner(): - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not' - ' binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not" + " binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): ml_g_classifier = LogisticRegression() _ = DoubleMLAPOS(dml_data, ml_g_classifier, ml_m, treatment_levels=0) @@ -51,27 +54,25 @@ def test_apos_exception_learner(): @pytest.mark.ci def test_apos_exception_scores(): - msg = 'Invalid score MAR. Valid score APO.' + msg = "Invalid score MAR. Valid score APO." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, score='MAR') + _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, score="MAR") @pytest.mark.ci def test_apos_exception_trimming_rule(): - msg = 'Invalid trimming_rule discard. Valid trimming_rule truncate.' + msg = "Invalid trimming_rule discard. Valid trimming_rule truncate." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, trimming_rule='discard') + _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, trimming_rule="discard") # check the trimming_threshold exceptions msg = "trimming_threshold has to be a float. Object of type passed." with pytest.raises(TypeError, match=msg): - _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, - trimming_rule='truncate', trimming_threshold="0.1") + _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, trimming_rule="truncate", trimming_threshold="0.1") - msg = 'Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5.' + msg = "Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5." with pytest.raises(ValueError, match=msg): - _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, - trimming_rule='truncate', trimming_threshold=0.6) + _ = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, trimming_rule="truncate", trimming_threshold=0.6) @pytest.mark.ci @@ -85,25 +86,25 @@ def test_apos_exception_ipw_normalization(): def test_apos_exception_properties_and_methods(): # properties dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0, draw_sample_splitting=False) - msg = r'Sample splitting not specified. Draw samples via .draw_sample splitting\(\). External samples not implemented yet.' + msg = r"Sample splitting not specified. Draw samples via .draw_sample splitting\(\). External samples not implemented yet." with pytest.raises(ValueError, match=msg): _ = dml_obj.smpls # methods dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0) - msg = r'Apply fit\(\) before confint\(\).' + msg = r"Apply fit\(\) before confint\(\)." with pytest.raises(ValueError, match=msg): dml_obj.confint() - msg = r'Apply fit\(\) before bootstrap\(\).' + msg = r"Apply fit\(\) before bootstrap\(\)." with pytest.raises(ValueError, match=msg): dml_obj.bootstrap() - msg = r'Apply fit\(\) before sensitivity_analysis\(\).' + msg = r"Apply fit\(\) before sensitivity_analysis\(\)." with pytest.raises(ValueError, match=msg): dml_obj.sensitivity_analysis() - msg = r'Apply fit\(\) before sensitivity_plot\(\).' + msg = r"Apply fit\(\) before sensitivity_plot\(\)." with pytest.raises(ValueError, match=msg): dml_obj.sensitivity_plot() - msg = r'Apply sensitivity_analysis\(\) before sensitivity_summary.' + msg = r"Apply sensitivity_analysis\(\) before sensitivity_summary." with pytest.raises(ValueError, match=msg): _ = dml_obj.sensitivity_summary @@ -112,15 +113,12 @@ def test_apos_exception_properties_and_methods(): def test_apos_exception_ext_pred(): dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=0) external_predictions = [0, 1] - msg = r'external_predictions must be a dictionary. Object of type passed.' + msg = r"external_predictions must be a dictionary. Object of type passed." with pytest.raises(TypeError, match=msg): dml_obj.fit(external_predictions=external_predictions) # test with a level subset - external_predictions = { - 0: "dummy", - 1: "dummy" - } + external_predictions = {0: "dummy", 1: "dummy"} msg = ( r"external_predictions must be a subset of all treatment levels\. " r"Expected keys: \{0\}\. " @@ -132,24 +130,18 @@ def test_apos_exception_ext_pred(): external_predictions = { 0: "dummy", } - msg = r'external_predictions\[0\] must be a dictionary. Object of type passed.' + msg = r"external_predictions\[0\] must be a dictionary. Object of type passed." with pytest.raises(TypeError, match=msg): dml_obj.fit(external_predictions=external_predictions) - external_predictions = { - 0: {"ml_g": "dummy"} - } + external_predictions = {0: {"ml_g": "dummy"}} msg = r"external_predictions\[0\] must be a subset of \{.*\}. Passed keys: \{'ml_g'\}\." with pytest.raises(ValueError, match=msg): dml_obj.fit(external_predictions=external_predictions) # test with all levels dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=[0, 1, 2, 3]) - external_predictions = { - 0: "dummy", - 1: "dummy", - 4: "dummy" - } + external_predictions = {0: "dummy", 1: "dummy", 4: "dummy"} msg = ( r"external_predictions must be a subset of all treatment levels\. " r"Expected keys: \{0, 1, 2, 3\}\. " @@ -166,7 +158,7 @@ def test_causal_contrast_exceptions(): dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=[0, 1]) dml_obj.causal_contrast(reference_levels=0) - msg = 'Only one treatment level. No causal contrast can be computed.' + msg = "Only one treatment level. No causal contrast can be computed." with pytest.raises(ValueError, match=msg): dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=[0]) dml_obj.fit() @@ -174,8 +166,10 @@ def test_causal_contrast_exceptions(): dml_obj = DoubleMLAPOS(dml_data, ml_g, ml_m, treatment_levels=[0, 1]) dml_obj.fit() - msg = ('Invalid reference_levels. reference_levels has to be an iterable subset of treatment_levels or ' - 'a single treatment level.') + msg = ( + "Invalid reference_levels. reference_levels has to be an iterable subset of treatment_levels or " + "a single treatment level." + ) with pytest.raises(ValueError, match=msg): dml_obj.causal_contrast(reference_levels=2) with pytest.raises(ValueError, match=msg): diff --git a/doubleml/irm/tests/test_apos_external_predictions.py b/doubleml/irm/tests/test_apos_external_predictions.py index 9657a83be..a6e2c9120 100644 --- a/doubleml/irm/tests/test_apos_external_predictions.py +++ b/doubleml/irm/tests/test_apos_external_predictions.py @@ -35,20 +35,18 @@ def set_ml_g_ext(request): @pytest.fixture(scope="module") def doubleml_apos_ext_fixture(n_rep, treatment_levels, set_ml_m_ext, set_ml_g_ext): score = "APO" - ext_predictions = { - treatment_level: {} for treatment_level in treatment_levels - } + ext_predictions = {treatment_level: {} for treatment_level in treatment_levels} np.random.seed(3141) n_obs = 500 data_apo = make_irm_data_discrete_treatments(n_obs=n_obs) df_apo = pd.DataFrame( - np.column_stack((data_apo['y'], data_apo['d'], data_apo['x'])), - columns=['y', 'd'] + ['x' + str(i) for i in range(data_apo['x'].shape[1])] + np.column_stack((data_apo["y"], data_apo["d"], data_apo["x"])), + columns=["y", "d"] + ["x" + str(i) for i in range(data_apo["x"].shape[1])], ) - dml_data = DoubleMLData(df_apo, 'y', 'd') - d = data_apo['d'] + dml_data = DoubleMLData(df_apo, "y", "d") + d = data_apo["d"] all_smpls = draw_smpls(n_obs, n_folds=5, n_rep=n_rep, groups=d) kwargs = { @@ -56,7 +54,7 @@ def doubleml_apos_ext_fixture(n_rep, treatment_levels, set_ml_m_ext, set_ml_g_ex "score": score, "treatment_levels": treatment_levels, "n_rep": n_rep, - "draw_sample_splitting": False + "draw_sample_splitting": False, } dml_obj = DoubleMLAPOS(ml_g=LinearRegression(), ml_m=LogisticRegression(), **kwargs) @@ -91,7 +89,7 @@ def doubleml_apos_ext_fixture(n_rep, treatment_levels, set_ml_m_ext, set_ml_g_ex "coef_ext": dml_obj_ext.coef[0], "dml_obj": dml_obj, "dml_obj_ext": dml_obj_ext, - "treatment_levels": treatment_levels + "treatment_levels": treatment_levels, } return res_dict @@ -100,10 +98,7 @@ def doubleml_apos_ext_fixture(n_rep, treatment_levels, set_ml_m_ext, set_ml_g_ex @pytest.mark.ci def test_doubleml_apos_ext_coef(doubleml_apos_ext_fixture): assert math.isclose( - doubleml_apos_ext_fixture["coef_normal"], - doubleml_apos_ext_fixture["coef_ext"], - rel_tol=1e-9, - abs_tol=1e-4 + doubleml_apos_ext_fixture["coef_normal"], doubleml_apos_ext_fixture["coef_ext"], rel_tol=1e-9, abs_tol=1e-4 ) @@ -115,5 +110,5 @@ def test_doubleml_apos_ext_pred_nuisance(doubleml_apos_ext_fixture): doubleml_apos_ext_fixture["dml_obj"].modellist[i_level].nuisance_loss[nuisance_key], doubleml_apos_ext_fixture["dml_obj_ext"].modellist[i_level].nuisance_loss[nuisance_key], rtol=1e-9, - atol=1e-4 + atol=1e-4, ) diff --git a/doubleml/irm/tests/test_apos_weighted_scores.py b/doubleml/irm/tests/test_apos_weighted_scores.py index 490f7a1aa..ea612decf 100644 --- a/doubleml/irm/tests/test_apos_weighted_scores.py +++ b/doubleml/irm/tests/test_apos_weighted_scores.py @@ -9,74 +9,70 @@ from doubleml.datasets import make_irm_data_discrete_treatments -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['APO']) +@pytest.fixture(scope="module", params=["APO"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=[[0, 1, 2], [0]]) +@pytest.fixture(scope="module", params=[[0, 1, 2], [0]]) def treatment_levels(request): return request.param -@pytest.fixture(scope='module') -def weighted_apos_score_fixture(learner, score, n_rep, normalize_ipw, trimming_threshold, - treatment_levels): +@pytest.fixture(scope="module") +def weighted_apos_score_fixture(learner, score, n_rep, normalize_ipw, trimming_threshold, treatment_levels): n_obs = 500 n_folds = 2 # collect data data = make_irm_data_discrete_treatments(n_obs=n_obs) - y = data['y'] - x = data['x'] - d = data['d'] - df = pd.DataFrame( - np.column_stack((y, d, x)), - columns=['y', 'd'] + ['x' + str(i) for i in range(data['x'].shape[1])] - ) + y = data["y"] + x = data["x"] + d = data["d"] + df = pd.DataFrame(np.column_stack((y, d, x)), columns=["y", "d"] + ["x" + str(i) for i in range(data["x"].shape[1])]) - obj_dml_data = dml.DoubleMLData(df, 'y', 'd') + obj_dml_data = dml.DoubleMLData(df, "y", "d") input_args = { - 'obj_dml_data': obj_dml_data, - 'ml_g': clone(learner[0]), - 'ml_m': clone(learner[1]), - 'treatment_levels': treatment_levels, - 'n_folds': n_folds, - 'n_rep': n_rep, - 'score': score, - 'normalize_ipw': normalize_ipw, - 'trimming_threshold': trimming_threshold, - 'trimming_rule': 'truncate' + "obj_dml_data": obj_dml_data, + "ml_g": clone(learner[0]), + "ml_m": clone(learner[1]), + "treatment_levels": treatment_levels, + "n_folds": n_folds, + "n_rep": n_rep, + "score": score, + "normalize_ipw": normalize_ipw, + "trimming_threshold": trimming_threshold, + "trimming_rule": "truncate", } np.random.seed(42) @@ -85,43 +81,38 @@ def weighted_apos_score_fixture(learner, score, n_rep, normalize_ipw, trimming_t np.random.seed(42) weights = 0.5 * np.ones_like(obj_dml_data.y) - dml_obj_weighted = dml.DoubleMLAPOS(draw_sample_splitting=False, - weights=weights, - **input_args) + dml_obj_weighted = dml.DoubleMLAPOS(draw_sample_splitting=False, weights=weights, **input_args) dml_obj_weighted.set_sample_splitting(all_smpls=dml_obj.smpls) dml_obj_weighted.fit() np.random.seed(42) weights_dict = { - 'weights': weights, - 'weights_bar': np.tile(weights[:, np.newaxis], (1, n_rep)), + "weights": weights, + "weights_bar": np.tile(weights[:, np.newaxis], (1, n_rep)), } - dml_obj_weighted_dict = dml.DoubleMLAPOS(draw_sample_splitting=False, - weights=weights_dict, - **input_args) + dml_obj_weighted_dict = dml.DoubleMLAPOS(draw_sample_splitting=False, weights=weights_dict, **input_args) dml_obj_weighted_dict.set_sample_splitting(all_smpls=dml_obj.smpls) dml_obj_weighted_dict.fit() result_dict = { - 'coef': dml_obj.coef, - 'weighted_coef': dml_obj_weighted.coef, - 'weighted_coef_dict': dml_obj_weighted_dict.coef, - 'default_weights': dml_obj.weights, + "coef": dml_obj.coef, + "weighted_coef": dml_obj_weighted.coef, + "weighted_coef_dict": dml_obj_weighted_dict.coef, + "default_weights": dml_obj.weights, } return result_dict @pytest.mark.ci def test_apos_weighted_coef(weighted_apos_score_fixture): - assert np.allclose(0.5 * weighted_apos_score_fixture['coef'], - weighted_apos_score_fixture['weighted_coef']) - assert np.allclose(0.5 * weighted_apos_score_fixture['coef'], - weighted_apos_score_fixture['weighted_coef_dict']) + assert np.allclose(0.5 * weighted_apos_score_fixture["coef"], weighted_apos_score_fixture["weighted_coef"]) + assert np.allclose(0.5 * weighted_apos_score_fixture["coef"], weighted_apos_score_fixture["weighted_coef_dict"]) @pytest.mark.ci def test_apos_default_weights(weighted_apos_score_fixture): - assert isinstance(weighted_apos_score_fixture['default_weights'], np.ndarray) + assert isinstance(weighted_apos_score_fixture["default_weights"], np.ndarray) - assert np.allclose(weighted_apos_score_fixture['default_weights'], - np.ones_like(weighted_apos_score_fixture['default_weights'])) + assert np.allclose( + weighted_apos_score_fixture["default_weights"], np.ones_like(weighted_apos_score_fixture["default_weights"]) + ) diff --git a/doubleml/irm/tests/test_cvar.py b/doubleml/irm/tests/test_cvar.py index 82927a1ef..0eee71c60 100644 --- a/doubleml/irm/tests/test_cvar.py +++ b/doubleml/irm/tests/test_cvar.py @@ -12,42 +12,42 @@ from ._utils_cvar_manual import fit_cvar -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment(request): return request.param -@pytest.fixture(scope='module', - params=[0.25, 0.5, 0.75]) +@pytest.fixture(scope="module", params=[0.25, 0.5, 0.75]) def quantile(request): return request.param -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param @pytest.fixture(scope="module") -def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner, - normalize_ipw, trimming_threshold): +def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner, normalize_ipw, trimming_threshold): n_folds = 3 # Set machine learning methods for m & g @@ -62,44 +62,53 @@ def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner, all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d) np.random.seed(42) - dml_cvar_obj = dml.DoubleMLCVAR(obj_dml_data, - clone(ml_g), clone(ml_m), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + dml_cvar_obj = dml.DoubleMLCVAR( + obj_dml_data, + clone(ml_g), + clone(ml_m), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_cvar_obj.set_sample_splitting(all_smpls=all_smpls) dml_cvar_obj.fit() np.random.seed(42) - res_manual = fit_cvar(y, x, d, quantile, - clone(ml_g), clone(ml_m), - all_smpls, treatment, - normalize_ipw=normalize_ipw, - n_rep=1, trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_cvar_obj.coef.item(), - 'coef_manual': res_manual['pq'], - 'se': dml_cvar_obj.se.item(), - 'se_manual': res_manual['se']} + res_manual = fit_cvar( + y, + x, + d, + quantile, + clone(ml_g), + clone(ml_m), + all_smpls, + treatment, + normalize_ipw=normalize_ipw, + n_rep=1, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_cvar_obj.coef.item(), + "coef_manual": res_manual["pq"], + "se": dml_cvar_obj.se.item(), + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_cvar_coef(dml_cvar_fixture): - assert math.isclose(dml_cvar_fixture['coef'], - dml_cvar_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_cvar_fixture["coef"], dml_cvar_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_cvar_se(dml_cvar_fixture): - assert math.isclose(dml_cvar_fixture['se'], - dml_cvar_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_cvar_fixture["se"], dml_cvar_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/irm/tests/test_cvar_tune.py b/doubleml/irm/tests/test_cvar_tune.py index 6d253dcc9..ade847691 100644 --- a/doubleml/irm/tests/test_cvar_tune.py +++ b/doubleml/irm/tests/test_cvar_tune.py @@ -11,53 +11,45 @@ from ._utils_cvar_manual import fit_cvar, tune_nuisance_cvar -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment(request): return request.param -@pytest.fixture(scope='module', - params=[0.25, 0.5, 0.75]) +@pytest.fixture(scope="module", params=[0.25, 0.5, 0.75]) def quantile(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestRegressor(max_depth=5, random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(max_depth=5, random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestClassifier(max_depth=5, random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestClassifier(max_depth=5, random_state=42)]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor, RandomForestClassifier]: - par_grid = {'n_estimators': [5, 10, 15, 20]} + par_grid = {"n_estimators": [5, 10, 15, 20]} return par_grid -@pytest.fixture(scope='module') -def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner_g, learner_m, - normalize_ipw, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} +@pytest.fixture(scope="module") +def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner_g, learner_m, normalize_ipw, tune_on_folds): + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 n_folds = 2 @@ -70,22 +62,24 @@ def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner_g, le smpls = all_smpls[0] np.random.seed(42) - dml_cvar_obj = dml.DoubleMLCVAR(obj_dml_data, - clone(learner_g), clone(learner_m), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - normalize_ipw=normalize_ipw, - trimming_threshold=0.01, - draw_sample_splitting=False) + dml_cvar_obj = dml.DoubleMLCVAR( + obj_dml_data, + clone(learner_g), + clone(learner_m), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + normalize_ipw=normalize_ipw, + trimming_threshold=0.01, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_cvar_obj.set_sample_splitting(all_smpls=all_smpls) # tune hyperparameters np.random.seed(42) - tune_res = dml_cvar_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_cvar_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLCVAR) np.random.seed(42) @@ -93,47 +87,70 @@ def dml_cvar_fixture(generate_data_quantiles, treatment, quantile, learner_g, le np.random.seed(42) if tune_on_folds: - g_params, m_params = tune_nuisance_cvar(y, x, d, - clone(learner_g), clone(learner_m), - smpls, treatment, quantile, - n_folds_tune, par_grid['ml_g'], par_grid['ml_m']) + g_params, m_params = tune_nuisance_cvar( + y, + x, + d, + clone(learner_g), + clone(learner_m), + smpls, + treatment, + quantile, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + ) else: xx = [(np.arange(len(y)), np.array([]))] - g_params, m_params = tune_nuisance_cvar(y, x, d, - clone(learner_g), clone(learner_m), - xx, treatment, quantile, - n_folds_tune, par_grid['ml_g'], par_grid['ml_m']) + g_params, m_params = tune_nuisance_cvar( + y, + x, + d, + clone(learner_g), + clone(learner_m), + xx, + treatment, + quantile, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + ) g_params = g_params * n_folds m_params = m_params * n_folds np.random.seed(42) - res_manual = fit_cvar(y, x, d, quantile, - learner_g=clone(learner_g), - learner_m=clone(learner_m), - all_smpls=all_smpls, - treatment=treatment, - n_rep=1, trimming_threshold=0.01, - normalize_ipw=normalize_ipw, - g_params=g_params, m_params=m_params) - - res_dict = {'coef': dml_cvar_obj.coef.item(), - 'coef_manual': res_manual['pq'], - 'se': dml_cvar_obj.se.item(), - 'se_manual': res_manual['se']} + res_manual = fit_cvar( + y, + x, + d, + quantile, + learner_g=clone(learner_g), + learner_m=clone(learner_m), + all_smpls=all_smpls, + treatment=treatment, + n_rep=1, + trimming_threshold=0.01, + normalize_ipw=normalize_ipw, + g_params=g_params, + m_params=m_params, + ) + + res_dict = { + "coef": dml_cvar_obj.coef.item(), + "coef_manual": res_manual["pq"], + "se": dml_cvar_obj.se.item(), + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_cvar_coef(dml_cvar_fixture): - assert math.isclose(dml_cvar_fixture['coef'], - dml_cvar_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_cvar_fixture["coef"], dml_cvar_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_cvar_se(dml_cvar_fixture): - assert math.isclose(dml_cvar_fixture['se'], - dml_cvar_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_cvar_fixture["se"], dml_cvar_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/irm/tests/test_iivm.py b/doubleml/irm/tests/test_iivm.py index 8826c64b7..169f4175d 100644 --- a/doubleml/irm/tests/test_iivm.py +++ b/doubleml/irm/tests/test_iivm.py @@ -12,46 +12,45 @@ from ._utils_iivm_manual import boot_iivm, fit_iivm -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=2, n_estimators=10), - RandomForestClassifier(max_depth=2, n_estimators=10)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [RandomForestRegressor(max_depth=2, n_estimators=10), RandomForestClassifier(max_depth=2, n_estimators=10)], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['LATE']) +@pytest.fixture(scope="module", params=["LATE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param @pytest.fixture(scope="module") def dml_iivm_fixture(generate_data_iivm, learner, score, normalize_ipw, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 491 # collect data data = generate_data_iivm - x_cols = data.columns[data.columns.str.startswith('X')].tolist() - y = data['y'].values + x_cols = data.columns[data.columns.str.startswith("X")].tolist() + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values - z = data['z'].values + d = data["d"].values + z = data["z"].values # Set machine learning methods for m & g ml_g = clone(learner[0]) @@ -64,64 +63,89 @@ def dml_iivm_fixture(generate_data_iivm, learner, score, normalize_ipw, trimming all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=strata) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'z') - dml_iivm_obj = dml.DoubleMLIIVM(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, - draw_sample_splitting=False, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, "z") + dml_iivm_obj = dml.DoubleMLIIVM( + obj_dml_data, + ml_g, + ml_m, + ml_r, + n_folds, + draw_sample_splitting=False, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_iivm_obj.set_sample_splitting(all_smpls=all_smpls) dml_iivm_obj.fit() np.random.seed(3141) - res_manual = fit_iivm(y, x, d, z, - clone(learner[0]), clone(learner[1]), clone(learner[1]), - all_smpls, score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_iivm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_iivm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_iivm( + y, + x, + d, + z, + clone(learner[0]), + clone(learner[1]), + clone(learner[1]), + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_iivm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_iivm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_iivm(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_r_hat0'], res_manual['all_r_hat1'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_iivm( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_r_hat0"], + res_manual["all_r_hat1"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_iivm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_iivm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_iivm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_iivm_coef(dml_iivm_fixture): - assert math.isclose(dml_iivm_fixture['coef'], - dml_iivm_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_iivm_fixture["coef"], dml_iivm_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_iivm_se(dml_iivm_fixture): - assert math.isclose(dml_iivm_fixture['se'], - dml_iivm_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_iivm_fixture["se"], dml_iivm_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_iivm_boot(dml_iivm_fixture): - for bootstrap in dml_iivm_fixture['boot_methods']: - assert np.allclose(dml_iivm_fixture['boot_t_stat' + bootstrap], - dml_iivm_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_iivm_fixture["boot_methods"]: + assert np.allclose( + dml_iivm_fixture["boot_t_stat" + bootstrap], + dml_iivm_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_iivm_classifier.py b/doubleml/irm/tests/test_iivm_classifier.py index 4efefde4f..983c34a77 100644 --- a/doubleml/irm/tests/test_iivm_classifier.py +++ b/doubleml/irm/tests/test_iivm_classifier.py @@ -12,36 +12,35 @@ from ._utils_iivm_manual import boot_iivm, fit_iivm -@pytest.fixture(scope='module', - params=[[LogisticRegression(solver='lbfgs', max_iter=250), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestClassifier(max_depth=2, n_estimators=10), - RandomForestClassifier(max_depth=2, n_estimators=10)]]) +@pytest.fixture( + scope="module", + params=[ + [LogisticRegression(solver="lbfgs", max_iter=250), LogisticRegression(solver="lbfgs", max_iter=250)], + [RandomForestClassifier(max_depth=2, n_estimators=10), RandomForestClassifier(max_depth=2, n_estimators=10)], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['LATE']) +@pytest.fixture(scope="module", params=["LATE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param @pytest.fixture(scope="module") def dml_iivm_classifier_fixture(generate_data_iivm_binary, learner, score, normalize_ipw, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 491 @@ -49,7 +48,7 @@ def dml_iivm_classifier_fixture(generate_data_iivm_binary, learner, score, norma (x, y, d, z) = generate_data_iivm_binary n_obs = len(y) - all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d+2*z) + all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d + 2 * z) # Set machine learning methods for m & g ml_g = clone(learner[0]) ml_m = clone(learner[1]) @@ -57,63 +56,92 @@ def dml_iivm_classifier_fixture(generate_data_iivm_binary, learner, score, norma np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, z) - dml_iivm_obj = dml.DoubleMLIIVM(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + dml_iivm_obj = dml.DoubleMLIIVM( + obj_dml_data, + ml_g, + ml_m, + ml_r, + n_folds, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_iivm_obj.set_sample_splitting(all_smpls=all_smpls) np.random.seed(3141) dml_iivm_obj.fit() np.random.seed(3141) - res_manual = fit_iivm(y, x, d, z, - clone(learner[0]), clone(learner[1]), clone(learner[1]), - all_smpls, score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_iivm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_iivm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_iivm( + y, + x, + d, + z, + clone(learner[0]), + clone(learner[1]), + clone(learner[1]), + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_iivm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_iivm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_iivm(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_r_hat0'], res_manual['all_r_hat1'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_iivm( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_r_hat0"], + res_manual["all_r_hat1"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_iivm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_iivm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_iivm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_iivm_coef(dml_iivm_classifier_fixture): - assert math.isclose(dml_iivm_classifier_fixture['coef'], - dml_iivm_classifier_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_iivm_classifier_fixture["coef"], dml_iivm_classifier_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_iivm_se(dml_iivm_classifier_fixture): - assert math.isclose(dml_iivm_classifier_fixture['se'], - dml_iivm_classifier_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_iivm_classifier_fixture["se"], dml_iivm_classifier_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_iivm_boot(dml_iivm_classifier_fixture): - for bootstrap in dml_iivm_classifier_fixture['boot_methods']: - assert np.allclose(dml_iivm_classifier_fixture['boot_t_stat' + bootstrap], - dml_iivm_classifier_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_iivm_classifier_fixture["boot_methods"]: + assert np.allclose( + dml_iivm_classifier_fixture["boot_t_stat" + bootstrap], + dml_iivm_classifier_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_iivm_external_predictions.py b/doubleml/irm/tests/test_iivm_external_predictions.py index 1b123baea..7f4626e95 100644 --- a/doubleml/irm/tests/test_iivm_external_predictions.py +++ b/doubleml/irm/tests/test_iivm_external_predictions.py @@ -18,9 +18,7 @@ def n_rep(request): def adapted_doubleml_fixture(n_rep): ext_predictions = {"d": {}} - data = make_iivm_data( - n_obs=500, dim_x=20, theta=0.5, alpha_x=1.0, return_type="DataFrame" - ) + data = make_iivm_data(n_obs=500, dim_x=20, theta=0.5, alpha_x=1.0, return_type="DataFrame") np.random.seed(3141) @@ -48,9 +46,7 @@ def adapted_doubleml_fixture(n_rep): ext_predictions["d"]["ml_r0"] = dml_iivm.predictions["ml_r0"][:, :, 0] ext_predictions["d"]["ml_r1"] = dml_iivm.predictions["ml_r1"][:, :, 0] - dml_iivm_ext = DoubleMLIIVM( - ml_g=DMLDummyRegressor(), ml_m=DMLDummyClassifier(), ml_r=DMLDummyClassifier(), **kwargs - ) + dml_iivm_ext = DoubleMLIIVM(ml_g=DMLDummyRegressor(), ml_m=DMLDummyClassifier(), ml_r=DMLDummyClassifier(), **kwargs) np.random.seed(3141) dml_iivm_ext.fit(external_predictions=ext_predictions) diff --git a/doubleml/irm/tests/test_iivm_subgroups.py b/doubleml/irm/tests/test_iivm_subgroups.py index 7a2e7b256..906ed8975 100644 --- a/doubleml/irm/tests/test_iivm_subgroups.py +++ b/doubleml/irm/tests/test_iivm_subgroups.py @@ -11,52 +11,53 @@ from ._utils_iivm_manual import boot_iivm, fit_iivm -@pytest.fixture(scope='module', - params=[[RandomForestRegressor(max_depth=2, n_estimators=10), - RandomForestClassifier(max_depth=2, n_estimators=10)]]) +@pytest.fixture( + scope="module", + params=[[RandomForestRegressor(max_depth=2, n_estimators=10), RandomForestClassifier(max_depth=2, n_estimators=10)]], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['LATE']) +@pytest.fixture(scope="module", params=["LATE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01]) +@pytest.fixture(scope="module", params=[0.01]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=[{'always_takers': True, 'never_takers': True}, - {'always_takers': False, 'never_takers': True}, - {'always_takers': True, 'never_takers': False}]) +@pytest.fixture( + scope="module", + params=[ + {"always_takers": True, "never_takers": True}, + {"always_takers": False, "never_takers": True}, + {"always_takers": True, "never_takers": False}, + ], +) def subgroups(request): return request.param @pytest.fixture(scope="module") -def dml_iivm_subgroups_fixture(generate_data_iivm, learner, score, normalize_ipw, - trimming_threshold, subgroups): - boot_methods = ['normal'] +def dml_iivm_subgroups_fixture(generate_data_iivm, learner, score, normalize_ipw, trimming_threshold, subgroups): + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 491 # collect data data = generate_data_iivm - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() - n_obs = len(data['y']) - strata = data['d'] + 2 * data['z'] + n_obs = len(data["y"]) + strata = data["d"] + 2 * data["z"] all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=strata) # Set machine learning methods for m & g @@ -65,83 +66,111 @@ def dml_iivm_subgroups_fixture(generate_data_iivm, learner, score, normalize_ipw ml_r = clone(learner[1]) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'z') - dml_iivm_obj = dml.DoubleMLIIVM(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, - subgroups=subgroups, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, "z") + dml_iivm_obj = dml.DoubleMLIIVM( + obj_dml_data, + ml_g, + ml_m, + ml_r, + n_folds, + subgroups=subgroups, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_iivm_obj.set_sample_splitting(all_smpls=all_smpls) dml_iivm_obj.fit(store_predictions=True) np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values - z = data['z'].values - - res_manual = fit_iivm(y, x, d, z, - clone(learner[0]), clone(learner[1]), clone(learner[1]), - all_smpls, score, - normalize_ipw=normalize_ipw, trimming_threshold=trimming_threshold, - always_takers=subgroups['always_takers'], never_takers=subgroups['never_takers']) - - res_dict = {'coef': dml_iivm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_iivm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods, - 'always_takers': subgroups['always_takers'], - 'never_takers': subgroups['never_takers'], - 'rhat0': dml_iivm_obj.predictions['ml_r0'], - 'rhat1': dml_iivm_obj.predictions['ml_r1'], - 'z': z - } + d = data["d"].values + z = data["z"].values + + res_manual = fit_iivm( + y, + x, + d, + z, + clone(learner[0]), + clone(learner[1]), + clone(learner[1]), + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + always_takers=subgroups["always_takers"], + never_takers=subgroups["never_takers"], + ) + + res_dict = { + "coef": dml_iivm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_iivm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + "always_takers": subgroups["always_takers"], + "never_takers": subgroups["never_takers"], + "rhat0": dml_iivm_obj.predictions["ml_r0"], + "rhat1": dml_iivm_obj.predictions["ml_r1"], + "z": z, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_iivm(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_r_hat0'], res_manual['all_r_hat1'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_iivm( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_r_hat0"], + res_manual["all_r_hat1"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_iivm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_iivm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_iivm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_iivm_subgroups_coef(dml_iivm_subgroups_fixture): - assert math.isclose(dml_iivm_subgroups_fixture['coef'], - dml_iivm_subgroups_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_iivm_subgroups_fixture["coef"], dml_iivm_subgroups_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_iivm_subgroups_se(dml_iivm_subgroups_fixture): - assert math.isclose(dml_iivm_subgroups_fixture['se'], - dml_iivm_subgroups_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_iivm_subgroups_fixture["se"], dml_iivm_subgroups_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_iivm_subgroups_boot(dml_iivm_subgroups_fixture): - for bootstrap in dml_iivm_subgroups_fixture['boot_methods']: - assert np.allclose(dml_iivm_subgroups_fixture['boot_t_stat' + bootstrap], - dml_iivm_subgroups_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_iivm_subgroups_fixture["boot_methods"]: + assert np.allclose( + dml_iivm_subgroups_fixture["boot_t_stat" + bootstrap], + dml_iivm_subgroups_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_iivm_subgroups(dml_iivm_subgroups_fixture): - if not dml_iivm_subgroups_fixture['always_takers']: - assert np.all(dml_iivm_subgroups_fixture['rhat0'] == 0) - if not dml_iivm_subgroups_fixture['never_takers']: - assert np.all(dml_iivm_subgroups_fixture['rhat1'] == 1) + if not dml_iivm_subgroups_fixture["always_takers"]: + assert np.all(dml_iivm_subgroups_fixture["rhat0"] == 0) + if not dml_iivm_subgroups_fixture["never_takers"]: + assert np.all(dml_iivm_subgroups_fixture["rhat1"] == 1) diff --git a/doubleml/irm/tests/test_iivm_tune.py b/doubleml/irm/tests/test_iivm_tune.py index 2ce28d801..aadbc31fd 100644 --- a/doubleml/irm/tests/test_iivm_tune.py +++ b/doubleml/irm/tests/test_iivm_tune.py @@ -12,76 +12,67 @@ from ._utils_iivm_manual import boot_iivm, fit_iivm, tune_nuisance_iivm -@pytest.fixture(scope='module', - params=[RandomForestRegressor()]) +@pytest.fixture(scope="module", params=[RandomForestRegressor()]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestClassifier()]) +@pytest.fixture(scope="module", params=[RandomForestClassifier()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression()]) +@pytest.fixture(scope="module", params=[LogisticRegression()]) def learner_r(request): return request.param -@pytest.fixture(scope='module', - params=['LATE']) +@pytest.fixture(scope="module", params=["LATE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[{'always_takers': True, 'never_takers': True}, - {'always_takers': False, 'never_takers': False}]) +@pytest.fixture( + scope="module", params=[{"always_takers": True, "never_takers": True}, {"always_takers": False, "never_takers": False}] +) def subgroups(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor, RandomForestClassifier]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ in [LogisticRegression] - par_grid = {'C': np.logspace(-4, 2, 10)} + par_grid = {"C": np.logspace(-4, 2, 10)} return par_grid @pytest.fixture(scope="module") -def dml_iivm_fixture(generate_data_iivm, learner_g, learner_m, learner_r, score, normalize_ipw, subgroups, - tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m), - 'ml_r': get_par_grid(learner_r)} +def dml_iivm_fixture(generate_data_iivm, learner_g, learner_m, learner_r, score, normalize_ipw, subgroups, tune_on_folds): + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m), "ml_r": get_par_grid(learner_r)} n_folds_tune = 4 - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 491 # collect data data = generate_data_iivm - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() - n_obs = len(data['y']) - strata = data['d'] + 2 * data['z'] + n_obs = len(data["y"]) + strata = data["d"] + 2 * data["z"] all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=strata) # Set machine learning methods for m, g & r @@ -90,107 +81,148 @@ def dml_iivm_fixture(generate_data_iivm, learner_g, learner_m, learner_r, score, ml_r = clone(learner_r) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'z') - dml_iivm_obj = dml.DoubleMLIIVM(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, - subgroups=subgroups, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, "z") + dml_iivm_obj = dml.DoubleMLIIVM( + obj_dml_data, ml_g, ml_m, ml_r, n_folds, subgroups=subgroups, normalize_ipw=normalize_ipw, draw_sample_splitting=False + ) # synchronize the sample splitting dml_iivm_obj.set_sample_splitting(all_smpls=all_smpls) # tune hyperparameters - tune_res = dml_iivm_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=True) + tune_res = dml_iivm_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=True) assert isinstance(tune_res, list) dml_iivm_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values - z = data['z'].values + d = data["d"].values + z = data["z"].values smpls = all_smpls[0] if tune_on_folds: - g0_params, g1_params, m_params, r0_params, r1_params = \ - tune_nuisance_iivm(y, x, d, z, - clone(learner_g), clone(learner_m), clone(learner_r), smpls, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], par_grid['ml_r'], - always_takers=subgroups['always_takers'], never_takers=subgroups['never_takers']) + g0_params, g1_params, m_params, r0_params, r1_params = tune_nuisance_iivm( + y, + x, + d, + z, + clone(learner_g), + clone(learner_m), + clone(learner_r), + smpls, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + par_grid["ml_r"], + always_takers=subgroups["always_takers"], + never_takers=subgroups["never_takers"], + ) else: xx = [(np.arange(data.shape[0]), np.array([]))] - g0_params, g1_params, m_params, r0_params, r1_params = \ - tune_nuisance_iivm(y, x, d, z, - clone(learner_g), clone(learner_m), clone(learner_r), xx, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], par_grid['ml_r'], - always_takers=subgroups['always_takers'], never_takers=subgroups['never_takers']) + g0_params, g1_params, m_params, r0_params, r1_params = tune_nuisance_iivm( + y, + x, + d, + z, + clone(learner_g), + clone(learner_m), + clone(learner_r), + xx, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + par_grid["ml_r"], + always_takers=subgroups["always_takers"], + never_takers=subgroups["never_takers"], + ) g0_params = g0_params * n_folds g1_params = g1_params * n_folds m_params = m_params * n_folds - if subgroups['always_takers']: + if subgroups["always_takers"]: r0_params = r0_params * n_folds else: r0_params = r0_params - if subgroups['never_takers']: + if subgroups["never_takers"]: r1_params = r1_params * n_folds else: r1_params = r1_params - res_manual = fit_iivm(y, x, d, z, - clone(learner_g), clone(learner_m), clone(learner_r), - all_smpls, score, - g0_params=g0_params, g1_params=g1_params, - m_params=m_params, r0_params=r0_params, r1_params=r1_params, - normalize_ipw=normalize_ipw, - always_takers=subgroups['always_takers'], never_takers=subgroups['never_takers']) - - res_dict = {'coef': dml_iivm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_iivm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_iivm( + y, + x, + d, + z, + clone(learner_g), + clone(learner_m), + clone(learner_r), + all_smpls, + score, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + r0_params=r0_params, + r1_params=r1_params, + normalize_ipw=normalize_ipw, + always_takers=subgroups["always_takers"], + never_takers=subgroups["never_takers"], + ) + + res_dict = { + "coef": dml_iivm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_iivm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_iivm(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_r_hat0'], res_manual['all_r_hat1'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_iivm( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_r_hat0"], + res_manual["all_r_hat1"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_iivm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_iivm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_iivm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci @pytest.mark.filterwarnings( - r'ignore:Propensity predictions from learner RandomForestClassifier\(\) for ml_m are close to zero or one ' - r'\(eps=1e-12\).:UserWarning' + r"ignore:Propensity predictions from learner RandomForestClassifier\(\) for ml_m are close to zero or one " + r"\(eps=1e-12\).:UserWarning" ) def test_dml_iivm_coef(dml_iivm_fixture): - assert math.isclose(dml_iivm_fixture['coef'], - dml_iivm_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_iivm_fixture["coef"], dml_iivm_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_iivm_se(dml_iivm_fixture): - assert math.isclose(dml_iivm_fixture['se'], - dml_iivm_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_iivm_fixture["se"], dml_iivm_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_iivm_boot(dml_iivm_fixture): - for bootstrap in dml_iivm_fixture['boot_methods']: - assert np.allclose(dml_iivm_fixture['boot_t_stat' + bootstrap], - dml_iivm_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_iivm_fixture["boot_methods"]: + assert np.allclose( + dml_iivm_fixture["boot_t_stat" + bootstrap], + dml_iivm_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_irm.py b/doubleml/irm/tests/test_irm.py index 9df51c424..0a9246264 100644 --- a/doubleml/irm/tests/test_irm.py +++ b/doubleml/irm/tests/test_irm.py @@ -15,36 +15,38 @@ from ._utils_irm_manual import boot_irm, fit_irm, fit_sensitivity_elements_irm -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['ATE', 'ATTE']) +@pytest.fixture(scope="module", params=["ATE", "ATTE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_irm_fixture(generate_data_irm, learner, score, normalize_ipw, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -61,133 +63,155 @@ def dml_irm_fixture(generate_data_irm, learner, score, normalize_ipw, trimming_t obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) np.random.seed(3141) - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False, - trimming_threshold=trimming_threshold) + dml_irm_obj = dml.DoubleMLIRM( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_irm_obj.set_sample_splitting(all_smpls=all_smpls) dml_irm_obj.fit() np.random.seed(3141) - res_manual = fit_irm(y, x, d, - clone(learner[0]), clone(learner[1]), - all_smpls, score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + res_manual = fit_irm( + y, + x, + d, + clone(learner[0]), + clone(learner[1]), + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) np.random.seed(3141) # test with external nuisance predictions - dml_irm_obj_ext = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False, - trimming_threshold=trimming_threshold) + dml_irm_obj_ext = dml.DoubleMLIRM( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + trimming_threshold=trimming_threshold, + ) # synchronize the sample splitting dml_irm_obj_ext.set_sample_splitting(all_smpls=all_smpls) - prediction_dict = {'d': {'ml_g0': dml_irm_obj.predictions['ml_g0'].reshape(-1, 1), - 'ml_g1': dml_irm_obj.predictions['ml_g1'].reshape(-1, 1), - 'ml_m': dml_irm_obj.predictions['ml_m'].reshape(-1, 1)}} + prediction_dict = { + "d": { + "ml_g0": dml_irm_obj.predictions["ml_g0"].reshape(-1, 1), + "ml_g1": dml_irm_obj.predictions["ml_g1"].reshape(-1, 1), + "ml_m": dml_irm_obj.predictions["ml_m"].reshape(-1, 1), + } + } dml_irm_obj_ext.fit(external_predictions=prediction_dict) - res_dict = {'coef': dml_irm_obj.coef, - 'coef_manual': res_manual['theta'], - 'coef_ext': dml_irm_obj_ext.coef, - 'se': dml_irm_obj.se, - 'se_manual': res_manual['se'], - 'se_ext': dml_irm_obj_ext.se, - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_irm_obj.coef, + "coef_manual": res_manual["theta"], + "coef_ext": dml_irm_obj_ext.coef, + "se": dml_irm_obj.se, + "se_manual": res_manual["se"], + "se_ext": dml_irm_obj_ext.se, + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_irm(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_p_hat'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_irm( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_p_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_irm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) np.random.seed(3141) dml_irm_obj_ext.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_irm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) - res_dict['boot_t_stat' + bootstrap + '_ext'] = dml_irm_obj_ext.boot_t_stat + res_dict["boot_t_stat" + bootstrap] = dml_irm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap + "_ext"] = dml_irm_obj_ext.boot_t_stat # sensitivity tests - res_dict['sensitivity_elements'] = dml_irm_obj.sensitivity_elements - res_dict['sensitivity_elements_manual'] = fit_sensitivity_elements_irm(y, d, - all_coef=dml_irm_obj.all_coef, - predictions=dml_irm_obj.predictions, - score=score, - n_rep=1) + res_dict["sensitivity_elements"] = dml_irm_obj.sensitivity_elements + res_dict["sensitivity_elements_manual"] = fit_sensitivity_elements_irm( + y, d, all_coef=dml_irm_obj.all_coef, predictions=dml_irm_obj.predictions, score=score, n_rep=1 + ) # check if sensitivity score with rho=0 gives equal asymptotic standard deviation dml_irm_obj.sensitivity_analysis(rho=0.0) - res_dict['sensitivity_ses'] = dml_irm_obj.sensitivity_params['se'] + res_dict["sensitivity_ses"] = dml_irm_obj.sensitivity_params["se"] return res_dict @pytest.mark.ci def test_dml_irm_coef(dml_irm_fixture): - assert math.isclose(dml_irm_fixture['coef'][0], - dml_irm_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_irm_fixture['coef'][0], - dml_irm_fixture['coef_ext'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_fixture["coef"][0], dml_irm_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_fixture["coef"][0], dml_irm_fixture["coef_ext"][0], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_irm_se(dml_irm_fixture): - assert math.isclose(dml_irm_fixture['se'][0], - dml_irm_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_irm_fixture['se'][0], - dml_irm_fixture['se_ext'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_fixture["se"][0], dml_irm_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_fixture["se"][0], dml_irm_fixture["se_ext"][0], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_irm_boot(dml_irm_fixture): - for bootstrap in dml_irm_fixture['boot_methods']: - assert np.allclose(dml_irm_fixture['boot_t_stat' + bootstrap], - dml_irm_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_irm_fixture['boot_t_stat' + bootstrap], - dml_irm_fixture['boot_t_stat' + bootstrap + '_ext'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_irm_fixture["boot_methods"]: + assert np.allclose( + dml_irm_fixture["boot_t_stat" + bootstrap], + dml_irm_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) + assert np.allclose( + dml_irm_fixture["boot_t_stat" + bootstrap], + dml_irm_fixture["boot_t_stat" + bootstrap + "_ext"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_irm_sensitivity(dml_irm_fixture): - sensitivity_element_names = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2'] + sensitivity_element_names = ["sigma2", "nu2", "psi_sigma2", "psi_nu2"] for sensitivity_element in sensitivity_element_names: - assert np.allclose(dml_irm_fixture['sensitivity_elements'][sensitivity_element], - dml_irm_fixture['sensitivity_elements_manual'][sensitivity_element], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_irm_fixture["sensitivity_elements"][sensitivity_element], + dml_irm_fixture["sensitivity_elements_manual"][sensitivity_element], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_irm_sensitivity_rho0(dml_irm_fixture): - assert np.allclose(dml_irm_fixture['se'], - dml_irm_fixture['sensitivity_ses']['lower'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_irm_fixture['se'], - dml_irm_fixture['sensitivity_ses']['upper'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_irm_fixture["se"], dml_irm_fixture["sensitivity_ses"]["lower"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_irm_fixture["se"], dml_irm_fixture["sensitivity_ses"]["upper"], rtol=1e-9, atol=1e-4) -@pytest.fixture(scope='module', - params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) +@pytest.fixture(scope="module", params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) def cov_type(request): return request.param @@ -206,11 +230,7 @@ def test_dml_irm_cate_gate(cov_type): ml_g = RandomForestRegressor(n_estimators=10) ml_m = RandomForestClassifier(n_estimators=10) - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_m=ml_m, - ml_g=ml_g, - trimming_threshold=0.05, - n_folds=5) + dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, ml_m=ml_m, ml_g=ml_g, trimming_threshold=0.05, n_folds=5) dml_irm_obj.fit() # create a random basis @@ -220,10 +240,10 @@ def test_dml_irm_cate_gate(cov_type): assert isinstance(cate.confint(), pd.DataFrame) assert cate.blp_model.cov_type == cov_type - groups_1 = pd.DataFrame(np.column_stack([obj_dml_data.data['X1'] <= 0, - obj_dml_data.data['X1'] > 0.2]), - columns=['Group 1', 'Group 2']) - msg = ('At least one group effect is estimated with less than 6 observations.') + groups_1 = pd.DataFrame( + np.column_stack([obj_dml_data.data["X1"] <= 0, obj_dml_data.data["X1"] > 0.2]), columns=["Group 1", "Group 2"] + ) + msg = "At least one group effect is estimated with less than 6 observations." with pytest.warns(UserWarning, match=msg): gate_1 = dml_irm_obj.gate(groups_1, cov_type=cov_type) assert isinstance(gate_1, dml.utils.blp.DoubleMLBLP) @@ -233,7 +253,7 @@ def test_dml_irm_cate_gate(cov_type): np.random.seed(42) groups_2 = pd.DataFrame(np.random.choice(["1", "2"], n)) - msg = ('At least one group effect is estimated with less than 6 observations.') + msg = "At least one group effect is estimated with less than 6 observations." with pytest.warns(UserWarning, match=msg): gate_2 = dml_irm_obj.gate(groups_2, cov_type=cov_type) assert isinstance(gate_2, dml.utils.blp.DoubleMLBLP) @@ -242,63 +262,40 @@ def test_dml_irm_cate_gate(cov_type): assert gate_2.blp_model.cov_type == cov_type -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_irm_weights_fixture(n_rep): n = 10000 # collect data np.random.seed(42) obj_dml_data = make_irm_data(n_obs=n, dim_x=2) - kwargs = { - "trimming_threshold": 0.05, - "n_folds": 5, - "n_rep": n_rep, - "draw_sample_splitting": False - } + kwargs = {"trimming_threshold": 0.05, "n_folds": 5, "n_rep": n_rep, "draw_sample_splitting": False} - smpls = DoubleMLResampling( - n_folds=5, - n_rep=n_rep, - n_obs=n, - stratify=obj_dml_data.d).split_samples() + smpls = DoubleMLResampling(n_folds=5, n_rep=n_rep, n_obs=n, stratify=obj_dml_data.d).split_samples() # First stage estimation ml_g = LinearRegression() - ml_m = LogisticRegression(penalty='l2', random_state=42) + ml_m = LogisticRegression(penalty="l2", random_state=42) # ATE with and without weights - dml_irm_obj_ate_no_weights = dml.DoubleMLIRM( - obj_dml_data, - ml_g=clone(ml_g), - ml_m=clone(ml_m), - score='ATE', - **kwargs) + dml_irm_obj_ate_no_weights = dml.DoubleMLIRM(obj_dml_data, ml_g=clone(ml_g), ml_m=clone(ml_m), score="ATE", **kwargs) dml_irm_obj_ate_no_weights.set_sample_splitting(smpls) np.random.seed(42) dml_irm_obj_ate_no_weights.fit() dml_irm_obj_ate_weights = dml.DoubleMLIRM( - obj_dml_data, - ml_g=clone(ml_g), - ml_m=clone(ml_m), - score='ATE', - weights=np.ones_like(obj_dml_data.y), **kwargs) + obj_dml_data, ml_g=clone(ml_g), ml_m=clone(ml_m), score="ATE", weights=np.ones_like(obj_dml_data.y), **kwargs + ) dml_irm_obj_ate_weights.set_sample_splitting(smpls) np.random.seed(42) dml_irm_obj_ate_weights.fit() # ATTE with and without weights - dml_irm_obj_atte_no_weights = dml.DoubleMLIRM( - obj_dml_data, - ml_g=clone(ml_g), - ml_m=clone(ml_m), - score='ATTE', - **kwargs) + dml_irm_obj_atte_no_weights = dml.DoubleMLIRM(obj_dml_data, ml_g=clone(ml_g), ml_m=clone(ml_m), score="ATTE", **kwargs) dml_irm_obj_atte_no_weights.set_sample_splitting(smpls) np.random.seed(42) dml_irm_obj_atte_no_weights.fit() @@ -307,46 +304,43 @@ def dml_irm_weights_fixture(n_rep): p_hat = obj_dml_data.d.mean() weights = obj_dml_data.d / p_hat weights_bar = m_hat / p_hat - weight_dict = {'weights': weights, 'weights_bar': weights_bar} + weight_dict = {"weights": weights, "weights_bar": weights_bar} dml_irm_obj_atte_weights = dml.DoubleMLIRM( - obj_dml_data, - ml_g=clone(ml_g), - ml_m=clone(ml_m), - score='ATE', - weights=weight_dict, **kwargs) + obj_dml_data, ml_g=clone(ml_g), ml_m=clone(ml_m), score="ATE", weights=weight_dict, **kwargs + ) dml_irm_obj_atte_weights.set_sample_splitting(smpls) np.random.seed(42) dml_irm_obj_atte_weights.fit() res_dict = { - 'coef_ate': dml_irm_obj_ate_no_weights.coef.item(), - 'coef_ate_weights': dml_irm_obj_ate_weights.coef.item(), - 'coef_atte': dml_irm_obj_atte_no_weights.coef.item(), - 'coef_atte_weights': dml_irm_obj_atte_weights.coef.item(), - 'se_ate': dml_irm_obj_ate_no_weights.se.item(), - 'se_ate_weights': dml_irm_obj_ate_weights.se.item(), - 'se_atte': dml_irm_obj_atte_no_weights.se.item(), - 'se_atte_weights': dml_irm_obj_atte_weights.se.item(), + "coef_ate": dml_irm_obj_ate_no_weights.coef.item(), + "coef_ate_weights": dml_irm_obj_ate_weights.coef.item(), + "coef_atte": dml_irm_obj_atte_no_weights.coef.item(), + "coef_atte_weights": dml_irm_obj_atte_weights.coef.item(), + "se_ate": dml_irm_obj_ate_no_weights.se.item(), + "se_ate_weights": dml_irm_obj_ate_weights.se.item(), + "se_atte": dml_irm_obj_atte_no_weights.se.item(), + "se_atte_weights": dml_irm_obj_atte_weights.se.item(), } return res_dict @pytest.mark.ci def test_dml_irm_ate_weights(dml_irm_weights_fixture): - assert math.isclose(dml_irm_weights_fixture['coef_ate'], - dml_irm_weights_fixture['coef_ate_weights'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_irm_weights_fixture['se_ate'], - dml_irm_weights_fixture['se_ate_weights'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_irm_weights_fixture["coef_ate"], dml_irm_weights_fixture["coef_ate_weights"], rel_tol=1e-9, abs_tol=1e-4 + ) + assert math.isclose( + dml_irm_weights_fixture["se_ate"], dml_irm_weights_fixture["se_ate_weights"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_irm_atte_weights(dml_irm_weights_fixture): - assert math.isclose(dml_irm_weights_fixture['coef_atte'], - dml_irm_weights_fixture['coef_atte_weights'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_irm_weights_fixture["coef_atte"], dml_irm_weights_fixture["coef_atte_weights"], rel_tol=1e-9, abs_tol=1e-4 + ) # Remark that the scores are slightly different (Y instead of g(1,X) and coefficient of theta) - assert math.isclose(dml_irm_weights_fixture['se_atte'], - dml_irm_weights_fixture['se_atte_weights'], - rel_tol=1e-5, abs_tol=1e-3) + assert math.isclose( + dml_irm_weights_fixture["se_atte"], dml_irm_weights_fixture["se_atte_weights"], rel_tol=1e-5, abs_tol=1e-3 + ) diff --git a/doubleml/irm/tests/test_irm_classifier.py b/doubleml/irm/tests/test_irm_classifier.py index af3472114..9389439d3 100644 --- a/doubleml/irm/tests/test_irm_classifier.py +++ b/doubleml/irm/tests/test_irm_classifier.py @@ -12,36 +12,38 @@ from ._utils_irm_manual import boot_irm, fit_irm -@pytest.fixture(scope='module', - params=[[LogisticRegression(solver='lbfgs', max_iter=250), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LogisticRegression(solver="lbfgs", max_iter=250), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['ATE', 'ATTE']) +@pytest.fixture(scope="module", params=["ATE", "ATTE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_irm_classifier_fixture(generate_data_irm_binary, learner, score, normalize_ipw, trimming_threshold): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -56,62 +58,85 @@ def dml_irm_classifier_fixture(generate_data_irm_binary, learner, score, normali np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + dml_irm_obj = dml.DoubleMLIRM( + obj_dml_data, + ml_g, + ml_m, + n_folds, + score=score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_irm_obj.set_sample_splitting(all_smpls=all_smpls) dml_irm_obj.fit() np.random.seed(3141) - res_manual = fit_irm(y, x, d, - clone(learner[0]), clone(learner[1]), - all_smpls, score, - normalize_ipw=normalize_ipw, trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_irm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_irm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_irm( + y, + x, + d, + clone(learner[0]), + clone(learner[1]), + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_irm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_irm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_irm(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_p_hat'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_irm( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_p_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_irm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_irm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_irm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_irm_coef(dml_irm_classifier_fixture): - assert math.isclose(dml_irm_classifier_fixture['coef'], - dml_irm_classifier_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_irm_classifier_fixture["coef"], dml_irm_classifier_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_irm_se(dml_irm_classifier_fixture): - assert math.isclose(dml_irm_classifier_fixture['se'], - dml_irm_classifier_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_classifier_fixture["se"], dml_irm_classifier_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_irm_boot(dml_irm_classifier_fixture): - for bootstrap in dml_irm_classifier_fixture['boot_methods']: - assert np.allclose(dml_irm_classifier_fixture['boot_t_stat' + bootstrap], - dml_irm_classifier_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_irm_classifier_fixture["boot_methods"]: + assert np.allclose( + dml_irm_classifier_fixture["boot_t_stat" + bootstrap], + dml_irm_classifier_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_irm_tune.py b/doubleml/irm/tests/test_irm_tune.py index aee7d098f..7e995f938 100644 --- a/doubleml/irm/tests/test_irm_tune.py +++ b/doubleml/irm/tests/test_irm_tune.py @@ -12,52 +12,46 @@ from ._utils_irm_manual import boot_irm, fit_irm, tune_nuisance_irm -@pytest.fixture(scope='module', - params=[RandomForestRegressor(random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression()]) +@pytest.fixture(scope="module", params=[LogisticRegression()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=['ATE', 'ATTE']) +@pytest.fixture(scope="module", params=["ATE", "ATTE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ in [LogisticRegression] - par_grid = {'C': np.logspace(-4, 2, 10)} + par_grid = {"C": np.logspace(-4, 2, 10)} return par_grid -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_irm_fixture(generate_data_irm, learner_g, learner_m, score, normalize_ipw, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -72,17 +66,12 @@ def dml_irm_fixture(generate_data_irm, learner_g, learner_m, score, normalize_ip np.random.seed(3141) obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - normalize_ipw=normalize_ipw) + dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, ml_g, ml_m, n_folds, score=score, normalize_ipw=normalize_ipw) # synchronize the sample splitting dml_irm_obj.set_sample_splitting(all_smpls=all_smpls) np.random.seed(3141) # tune hyperparameters - tune_res = dml_irm_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_irm_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLIRM) dml_irm_obj.fit() @@ -91,68 +80,86 @@ def dml_irm_fixture(generate_data_irm, learner_g, learner_m, score, normalize_ip smpls = all_smpls[0] if tune_on_folds: - g0_params, g1_params, m_params = tune_nuisance_irm(y, x, d, - clone(learner_g), clone(learner_m), smpls, score, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g0_params, g1_params, m_params = tune_nuisance_irm( + y, x, d, clone(learner_g), clone(learner_m), smpls, score, n_folds_tune, par_grid["ml_g"], par_grid["ml_m"] + ) else: xx = [(np.arange(len(y)), np.array([]))] - g0_params, g1_params, m_params = tune_nuisance_irm(y, x, d, - clone(learner_g), clone(learner_m), xx, score, - n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g0_params, g1_params, m_params = tune_nuisance_irm( + y, x, d, clone(learner_g), clone(learner_m), xx, score, n_folds_tune, par_grid["ml_g"], par_grid["ml_m"] + ) g0_params = g0_params * n_folds m_params = m_params * n_folds - if score == 'ATE': + if score == "ATE": g1_params = g1_params * n_folds else: - assert score == 'ATTE' + assert score == "ATTE" g1_params = None - res_manual = fit_irm(y, x, d, clone(learner_g), clone(learner_m), - all_smpls, score, - normalize_ipw=normalize_ipw, - g0_params=g0_params, g1_params=g1_params, m_params=m_params) - - res_dict = {'coef': dml_irm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_irm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_irm( + y, + x, + d, + clone(learner_g), + clone(learner_m), + all_smpls, + score, + normalize_ipw=normalize_ipw, + g0_params=g0_params, + g1_params=g1_params, + m_params=m_params, + ) + + res_dict = { + "coef": dml_irm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_irm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_irm(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_p_hat'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_irm( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_p_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_irm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_irm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_irm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_irm_coef(dml_irm_fixture): - assert math.isclose(dml_irm_fixture['coef'], - dml_irm_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_fixture["coef"], dml_irm_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_irm_se(dml_irm_fixture): - assert math.isclose(dml_irm_fixture['se'], - dml_irm_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_fixture["se"], dml_irm_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_irm_boot(dml_irm_fixture): - for bootstrap in dml_irm_fixture['boot_methods']: - assert np.allclose(dml_irm_fixture['boot_t_stat' + bootstrap], - dml_irm_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_irm_fixture["boot_methods"]: + assert np.allclose( + dml_irm_fixture["boot_t_stat" + bootstrap], + dml_irm_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/irm/tests/test_irm_weighted_scores.py b/doubleml/irm/tests/test_irm_weighted_scores.py index a6dd67904..a79844788 100644 --- a/doubleml/irm/tests/test_irm_weighted_scores.py +++ b/doubleml/irm/tests/test_irm_weighted_scores.py @@ -11,7 +11,7 @@ def old_score_elements(y, d, g_hat0, g_hat1, m_hat, score, normalize_ipw): # fraction of treated for ATTE p_hat = None - if score == 'ATTE': + if score == "ATTE": p_hat = np.mean(d) if normalize_ipw: @@ -20,54 +20,56 @@ def old_score_elements(y, d, g_hat0, g_hat1, m_hat, score, normalize_ipw): # compute residuals u_hat0 = y - g_hat0 u_hat1 = None - if score == 'ATE': + if score == "ATE": u_hat1 = y - g_hat1 psi_a = np.full_like(y, np.nan) psi_b = np.full_like(y, np.nan) - if score == 'ATE': - psi_b = g_hat1 - g_hat0 \ - + np.divide(np.multiply(d, u_hat1), m_hat) \ - - np.divide(np.multiply(1.0-d, u_hat0), 1.0 - m_hat) + if score == "ATE": + psi_b = ( + g_hat1 - g_hat0 + np.divide(np.multiply(d, u_hat1), m_hat) - np.divide(np.multiply(1.0 - d, u_hat0), 1.0 - m_hat) + ) psi_a = np.full_like(m_hat, -1.0) else: - assert score == 'ATTE' - psi_b = np.divide(np.multiply(d, u_hat0), p_hat) \ - - np.divide(np.multiply(m_hat, np.multiply(1.0-d, u_hat0)), - np.multiply(p_hat, (1.0 - m_hat))) - psi_a = - np.divide(d, p_hat) + assert score == "ATTE" + psi_b = np.divide(np.multiply(d, u_hat0), p_hat) - np.divide( + np.multiply(m_hat, np.multiply(1.0 - d, u_hat0)), np.multiply(p_hat, (1.0 - m_hat)) + ) + psi_a = -np.divide(d, p_hat) return psi_a, psi_b -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), - RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [ + RandomForestRegressor(max_depth=5, n_estimators=10, random_state=42), + RandomForestClassifier(max_depth=5, n_estimators=10, random_state=42), + ], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['ATE', 'ATTE']) +@pytest.fixture(scope="module", params=["ATE", "ATTE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[False, True]) +@pytest.fixture(scope="module", params=[False, True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.2, 0.15]) +@pytest.fixture(scope="module", params=[0.2, 0.15]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def old_vs_weighted_score_fixture(generate_data_irm, learner, score, normalize_ipw, trimming_threshold): n_folds = 2 @@ -80,51 +82,45 @@ def old_vs_weighted_score_fixture(generate_data_irm, learner, score, normalize_i ml_m = clone(learner[1]) np.random.seed(3141) - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + dml_irm_obj = dml.DoubleMLIRM( + obj_dml_data, ml_g, ml_m, n_folds, score=score, normalize_ipw=normalize_ipw, trimming_threshold=trimming_threshold + ) dml_irm_obj.fit() # old score psi_a_old, psi_b_old = old_score_elements( y=y, d=d, - g_hat0=np.squeeze(dml_irm_obj.predictions['ml_g0']), - g_hat1=np.squeeze(dml_irm_obj.predictions['ml_g1']), - m_hat=np.squeeze(dml_irm_obj.predictions['ml_m']), + g_hat0=np.squeeze(dml_irm_obj.predictions["ml_g0"]), + g_hat1=np.squeeze(dml_irm_obj.predictions["ml_g1"]), + m_hat=np.squeeze(dml_irm_obj.predictions["ml_m"]), score=score, - normalize_ipw=normalize_ipw + normalize_ipw=normalize_ipw, ) old_coef = -np.mean(psi_b_old) / np.mean(psi_a_old) result_dict = { - 'psi_a': np.squeeze(dml_irm_obj.psi_elements['psi_a']), - 'psi_b': np.squeeze(dml_irm_obj.psi_elements['psi_b']), - 'psi_a_old': psi_a_old, - 'psi_b_old': psi_b_old, - 'coef': np.squeeze(dml_irm_obj.coef), - 'old_coef': old_coef, + "psi_a": np.squeeze(dml_irm_obj.psi_elements["psi_a"]), + "psi_b": np.squeeze(dml_irm_obj.psi_elements["psi_b"]), + "psi_a_old": psi_a_old, + "psi_b_old": psi_b_old, + "coef": np.squeeze(dml_irm_obj.coef), + "old_coef": old_coef, } return result_dict @pytest.mark.ci def test_irm_old_vs_weighted_score_psi_b(old_vs_weighted_score_fixture): - assert np.allclose(old_vs_weighted_score_fixture['psi_b'], - old_vs_weighted_score_fixture['psi_b_old']) + assert np.allclose(old_vs_weighted_score_fixture["psi_b"], old_vs_weighted_score_fixture["psi_b_old"]) @pytest.mark.ci def test_irm_old_vs_weighted_score_psi_a(old_vs_weighted_score_fixture): - assert np.allclose(old_vs_weighted_score_fixture['psi_a'], - old_vs_weighted_score_fixture['psi_a_old']) + assert np.allclose(old_vs_weighted_score_fixture["psi_a"], old_vs_weighted_score_fixture["psi_a_old"]) @pytest.mark.ci def test_irm_old_vs_weighted_coef(old_vs_weighted_score_fixture): - assert np.allclose(old_vs_weighted_score_fixture['coef'], - old_vs_weighted_score_fixture['old_coef']) + assert np.allclose(old_vs_weighted_score_fixture["coef"], old_vs_weighted_score_fixture["old_coef"]) diff --git a/doubleml/irm/tests/test_irm_with_missings.py b/doubleml/irm/tests/test_irm_with_missings.py index d749ba5a5..a6c30cae8 100644 --- a/doubleml/irm/tests/test_irm_with_missings.py +++ b/doubleml/irm/tests/test_irm_with_missings.py @@ -14,45 +14,42 @@ from ._utils_irm_manual import boot_irm, fit_irm -@pytest.fixture(scope='module', - params=[[XGBRegressor(n_jobs=1, objective="reg:squarederror", - eta=0.1, n_estimators=10), - XGBClassifier(n_jobs=1, - objective="binary:logistic", eval_metric="logloss", - eta=0.1, n_estimators=10)]]) +@pytest.fixture( + scope="module", + params=[ + [ + XGBRegressor(n_jobs=1, objective="reg:squarederror", eta=0.1, n_estimators=10), + XGBClassifier(n_jobs=1, objective="binary:logistic", eval_metric="logloss", eta=0.1, n_estimators=10), + ] + ], +) def learner_xgboost(request): return request.param -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)]]) +@pytest.fixture(scope="module", params=[[LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)]]) def learner_sklearn(request): return request.param -@pytest.fixture(scope='module', - params=['ATE', 'ATTE']) +@pytest.fixture(scope="module", params=["ATE", "ATTE"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') -def dml_irm_w_missing_fixture(generate_data_irm_w_missings, learner_xgboost, score, - normalize_ipw, trimming_threshold): - boot_methods = ['normal'] +@pytest.fixture(scope="module") +def dml_irm_w_missing_fixture(generate_data_irm_w_missings, learner_xgboost, score, normalize_ipw, trimming_threshold): + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 499 @@ -67,68 +64,83 @@ def dml_irm_w_missing_fixture(generate_data_irm_w_missings, learner_xgboost, sco ml_m = clone(learner_xgboost[1]) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, - force_all_x_finite='allow-nan') - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) + obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, force_all_x_finite="allow-nan") + dml_irm_obj = dml.DoubleMLIRM( + obj_dml_data, ml_g, ml_m, n_folds, score=score, normalize_ipw=normalize_ipw, trimming_threshold=trimming_threshold + ) # synchronize the sample splitting dml_irm_obj.set_sample_splitting(all_smpls=all_smpls) np.random.seed(3141) dml_irm_obj.fit() np.random.seed(3141) - res_manual = fit_irm(y, x, d, - clone(learner_xgboost[0]), clone(learner_xgboost[1]), - all_smpls, score, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_irm_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_irm_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_irm( + y, + x, + d, + clone(learner_xgboost[0]), + clone(learner_xgboost[1]), + all_smpls, + score, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_irm_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_irm_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_irm(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat0'], res_manual['all_g_hat1'], - res_manual['all_m_hat'], res_manual['all_p_hat'], - all_smpls, score, bootstrap, n_rep_boot, - normalize_ipw=normalize_ipw) + boot_t_stat = boot_irm( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_g_hat0"], + res_manual["all_g_hat1"], + res_manual["all_m_hat"], + res_manual["all_p_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + normalize_ipw=normalize_ipw, + ) np.random.seed(3141) dml_irm_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_irm_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_irm_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_irm_w_missing_coef(dml_irm_w_missing_fixture): - assert math.isclose(dml_irm_w_missing_fixture['coef'], - dml_irm_w_missing_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_irm_w_missing_fixture["coef"], dml_irm_w_missing_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_irm_w_missing_se(dml_irm_w_missing_fixture): - assert math.isclose(dml_irm_w_missing_fixture['se'], - dml_irm_w_missing_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_irm_w_missing_fixture["se"], dml_irm_w_missing_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_irm_w_missing_boot(dml_irm_w_missing_fixture): - for bootstrap in dml_irm_w_missing_fixture['boot_methods']: - assert np.allclose(dml_irm_w_missing_fixture['boot_t_stat' + bootstrap], - dml_irm_w_missing_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_irm_w_missing_fixture["boot_methods"]: + assert np.allclose( + dml_irm_w_missing_fixture["boot_t_stat" + bootstrap], + dml_irm_w_missing_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) def test_irm_exception_with_missings(generate_data_irm_w_missings, learner_sklearn): @@ -140,10 +152,8 @@ def test_irm_exception_with_missings(generate_data_irm_w_missings, learner_sklea ml_m = clone(learner_sklearn[1]) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, - force_all_x_finite='allow-nan') - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m) + obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, force_all_x_finite="allow-nan") + dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, ml_g, ml_m) msg = r"Input X contains NaN.\nLinearRegression does not accept missing values encoded as NaN natively." with pytest.raises(ValueError, match=msg): diff --git a/doubleml/irm/tests/test_lpq.py b/doubleml/irm/tests/test_lpq.py index 2d29d0ee4..3e0049b80 100644 --- a/doubleml/irm/tests/test_lpq.py +++ b/doubleml/irm/tests/test_lpq.py @@ -15,50 +15,43 @@ def custom_kde(u, weights): dens = KDEUnivariate(u) - dens.fit(kernel='epa', bw='silverman', weights=weights, fft=False) + dens.fit(kernel="epa", bw="silverman", weights=weights, fft=False) return dens.evaluate(0) -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment(request): return request.param -@pytest.fixture(scope='module', - params=[0.25, 0.75]) +@pytest.fixture(scope="module", params=[0.25, 0.75]) def quantile(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression()]) +@pytest.fixture(scope="module", params=[LogisticRegression()]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.05]) +@pytest.fixture(scope="module", params=[0.05]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module', - params=['default', custom_kde]) +@pytest.fixture(scope="module", params=["default", custom_kde]) def kde(request): return request.param @pytest.fixture(scope="module") -def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, - normalize_ipw, trimming_threshold, kde): +def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, normalize_ipw, trimming_threshold, kde): n_folds = 3 # collect data @@ -70,64 +63,90 @@ def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=strata) np.random.seed(42) - if kde == 'default': - dml_lpq_obj = dml.DoubleMLLPQ(obj_dml_data, - clone(learner), clone(learner), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - normalize_ipw=normalize_ipw, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + if kde == "default": + dml_lpq_obj = dml.DoubleMLLPQ( + obj_dml_data, + clone(learner), + clone(learner), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + normalize_ipw=normalize_ipw, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_lpq_obj.set_sample_splitting(all_smpls=all_smpls) dml_lpq_obj.fit() np.random.seed(42) - res_manual = fit_lpq(y, x, d, z, quantile, clone(learner), clone(learner), - all_smpls, treatment, - normalize_ipw=normalize_ipw, kde=_default_kde, - n_rep=1, trimming_threshold=trimming_threshold) + res_manual = fit_lpq( + y, + x, + d, + z, + quantile, + clone(learner), + clone(learner), + all_smpls, + treatment, + normalize_ipw=normalize_ipw, + kde=_default_kde, + n_rep=1, + trimming_threshold=trimming_threshold, + ) else: - dml_lpq_obj = dml.DoubleMLLPQ(obj_dml_data, - clone(learner), clone(learner), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - normalize_ipw=normalize_ipw, - kde=kde, - trimming_threshold=trimming_threshold, - draw_sample_splitting=False) + dml_lpq_obj = dml.DoubleMLLPQ( + obj_dml_data, + clone(learner), + clone(learner), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + normalize_ipw=normalize_ipw, + kde=kde, + trimming_threshold=trimming_threshold, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_lpq_obj.set_sample_splitting(all_smpls=all_smpls) dml_lpq_obj.fit() np.random.seed(42) - res_manual = fit_lpq(y, x, d, z, quantile, clone(learner), clone(learner), - all_smpls, treatment, - normalize_ipw=normalize_ipw, kde=kde, - n_rep=1, trimming_threshold=trimming_threshold) - - res_dict = {'coef': dml_lpq_obj.coef.item(), - 'coef_manual': res_manual['lpq'], - 'se': dml_lpq_obj.se.item(), - 'se_manual': res_manual['se']} + res_manual = fit_lpq( + y, + x, + d, + z, + quantile, + clone(learner), + clone(learner), + all_smpls, + treatment, + normalize_ipw=normalize_ipw, + kde=kde, + n_rep=1, + trimming_threshold=trimming_threshold, + ) + + res_dict = { + "coef": dml_lpq_obj.coef.item(), + "coef_manual": res_manual["lpq"], + "se": dml_lpq_obj.se.item(), + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_lpq_coef(dml_lpq_fixture): - assert math.isclose(dml_lpq_fixture['coef'], - dml_lpq_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_lpq_fixture["coef"], dml_lpq_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_lpq_se(dml_lpq_fixture): - assert math.isclose(dml_lpq_fixture['se'], - dml_lpq_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_lpq_fixture["se"], dml_lpq_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/irm/tests/test_lpq_external_predictions.py b/doubleml/irm/tests/test_lpq_external_predictions.py index 7824d5491..66f2ece6e 100644 --- a/doubleml/irm/tests/test_lpq_external_predictions.py +++ b/doubleml/irm/tests/test_lpq_external_predictions.py @@ -59,10 +59,7 @@ def doubleml_lpq_fixture(n_rep, normalize_ipw): np.random.seed(3141) dml_lpq_ext.fit(external_predictions=ext_predictions) - res_dict = { - "coef_normal": dml_lpq.coef.item(), - "coef_ext": dml_lpq_ext.coef.item() - } + res_dict = {"coef_normal": dml_lpq.coef.item(), "coef_ext": dml_lpq_ext.coef.item()} return res_dict diff --git a/doubleml/irm/tests/test_lpq_tune.py b/doubleml/irm/tests/test_lpq_tune.py index 18741bd17..c2b7d1923 100644 --- a/doubleml/irm/tests/test_lpq_tune.py +++ b/doubleml/irm/tests/test_lpq_tune.py @@ -11,50 +11,46 @@ from ._utils_lpq_manual import fit_lpq, tune_nuisance_lpq -@pytest.fixture(scope='module', - params=[0]) +@pytest.fixture(scope="module", params=[0]) def treatment(request): return request.param -@pytest.fixture(scope='module', - params=[0.5]) +@pytest.fixture(scope="module", params=[0.5]) def quantile(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestClassifier(max_depth=2, n_estimators=5, random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestClassifier(max_depth=2, n_estimators=5, random_state=42)]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[True]) +@pytest.fixture(scope="module", params=[True]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestClassifier]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} return par_grid -@pytest.fixture(scope='module') -def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, normalize_ipw, - tune_on_folds): - par_grid = {'ml_m_z': get_par_grid(learner), - 'ml_m_d_z0': get_par_grid(learner), - 'ml_m_d_z1': get_par_grid(learner), - 'ml_g_du_z0': get_par_grid(learner), - 'ml_g_du_z1': get_par_grid(learner)} +@pytest.fixture(scope="module") +def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, normalize_ipw, tune_on_folds): + par_grid = { + "ml_m_z": get_par_grid(learner), + "ml_m_d_z0": get_par_grid(learner), + "ml_m_d_z1": get_par_grid(learner), + "ml_g_du_z0": get_par_grid(learner), + "ml_g_du_z1": get_par_grid(learner), + } n_folds_tune = 4 n_folds = 2 @@ -68,15 +64,18 @@ def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, smpls = all_smpls[0] np.random.seed(42) - dml_lpq_obj = dml.DoubleMLLPQ(obj_dml_data, - clone(learner), clone(learner), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - normalize_ipw=normalize_ipw, - trimming_threshold=0.01, - draw_sample_splitting=False) + dml_lpq_obj = dml.DoubleMLLPQ( + obj_dml_data, + clone(learner), + clone(learner), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + normalize_ipw=normalize_ipw, + trimming_threshold=0.01, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_lpq_obj.set_sample_splitting(all_smpls=all_smpls) @@ -90,26 +89,48 @@ def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, np.random.seed(42) if tune_on_folds: - m_z_params, m_d_z0_params, m_d_z1_params, \ - g_du_z0_params, g_du_z1_params = tune_nuisance_lpq(y, x, d, z, - clone(learner), - clone(learner), clone(learner), - clone(learner), clone(learner), - smpls, treatment, quantile, n_folds_tune, - par_grid['ml_m_z'], - par_grid['ml_m_d_z0'], par_grid['ml_m_d_z1'], - par_grid['ml_g_du_z0'], par_grid['ml_g_du_z1']) + m_z_params, m_d_z0_params, m_d_z1_params, g_du_z0_params, g_du_z1_params = tune_nuisance_lpq( + y, + x, + d, + z, + clone(learner), + clone(learner), + clone(learner), + clone(learner), + clone(learner), + smpls, + treatment, + quantile, + n_folds_tune, + par_grid["ml_m_z"], + par_grid["ml_m_d_z0"], + par_grid["ml_m_d_z1"], + par_grid["ml_g_du_z0"], + par_grid["ml_g_du_z1"], + ) else: xx = [(np.arange(len(y)), np.array([]))] - m_z_params, m_d_z0_params, m_d_z1_params, \ - g_du_z0_params, g_du_z1_params = tune_nuisance_lpq(y, x, d, z, - clone(learner), - clone(learner), clone(learner), - clone(learner), clone(learner), - xx, treatment, quantile, n_folds_tune, - par_grid['ml_m_z'], - par_grid['ml_m_d_z0'], par_grid['ml_m_d_z1'], - par_grid['ml_g_du_z0'], par_grid['ml_g_du_z1']) + m_z_params, m_d_z0_params, m_d_z1_params, g_du_z0_params, g_du_z1_params = tune_nuisance_lpq( + y, + x, + d, + z, + clone(learner), + clone(learner), + clone(learner), + clone(learner), + clone(learner), + xx, + treatment, + quantile, + n_folds_tune, + par_grid["ml_m_z"], + par_grid["ml_m_d_z0"], + par_grid["ml_m_d_z1"], + par_grid["ml_g_du_z0"], + par_grid["ml_g_du_z1"], + ) m_z_params = m_z_params * n_folds m_d_z0_params = m_d_z0_params * n_folds @@ -118,35 +139,41 @@ def dml_lpq_fixture(generate_data_local_quantiles, treatment, quantile, learner, g_du_z1_params = g_du_z1_params * n_folds np.random.seed(42) - res_manual = fit_lpq(y, x, d, z, - quantile=quantile, - learner_g=clone(learner), - learner_m=clone(learner), - all_smpls=all_smpls, - treatment=treatment, - n_rep=1, trimming_threshold=0.01, - normalize_ipw=normalize_ipw, - m_z_params=m_z_params, - m_d_z0_params=m_d_z0_params, m_d_z1_params=m_d_z1_params, - g_du_z0_params=g_du_z0_params, g_du_z1_params=g_du_z1_params) - - res_dict = {'coef': dml_lpq_obj.coef.item(), - 'coef_manual': res_manual['lpq'], - 'se': dml_lpq_obj.se.item(), - 'se_manual': res_manual['se']} + res_manual = fit_lpq( + y, + x, + d, + z, + quantile=quantile, + learner_g=clone(learner), + learner_m=clone(learner), + all_smpls=all_smpls, + treatment=treatment, + n_rep=1, + trimming_threshold=0.01, + normalize_ipw=normalize_ipw, + m_z_params=m_z_params, + m_d_z0_params=m_d_z0_params, + m_d_z1_params=m_d_z1_params, + g_du_z0_params=g_du_z0_params, + g_du_z1_params=g_du_z1_params, + ) + + res_dict = { + "coef": dml_lpq_obj.coef.item(), + "coef_manual": res_manual["lpq"], + "se": dml_lpq_obj.se.item(), + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_lpq_coef(dml_lpq_fixture): - assert math.isclose(dml_lpq_fixture['coef'], - dml_lpq_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_lpq_fixture["coef"], dml_lpq_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_lpq_se(dml_lpq_fixture): - assert math.isclose(dml_lpq_fixture['se'], - dml_lpq_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_lpq_fixture["se"], dml_lpq_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/irm/tests/test_pq.py b/doubleml/irm/tests/test_pq.py index 50fc8c613..62e69d532 100644 --- a/doubleml/irm/tests/test_pq.py +++ b/doubleml/irm/tests/test_pq.py @@ -12,40 +12,35 @@ from ._utils_pq_manual import fit_pq -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment(request): return request.param -@pytest.fixture(scope='module', - params=[0.25, 0.5, 0.75]) +@pytest.fixture(scope="module", params=[0.25, 0.5, 0.75]) def quantile(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), - LogisticRegression()]) +@pytest.fixture( + scope="module", params=[RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), LogisticRegression()] +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param @pytest.fixture(scope="module") -def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner, - normalize_ipw, trimming_threshold): +def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner, normalize_ipw, trimming_threshold): n_folds = 3 # collect data @@ -56,15 +51,18 @@ def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner, all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d) np.random.seed(42) - dml_pq_obj = dml.DoubleMLPQ(obj_dml_data, - clone(learner), clone(learner), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - trimming_threshold=trimming_threshold, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) + dml_pq_obj = dml.DoubleMLPQ( + obj_dml_data, + clone(learner), + clone(learner), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + trimming_threshold=trimming_threshold, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_pq_obj.set_sample_splitting(all_smpls=all_smpls) @@ -72,30 +70,35 @@ def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner, dml_pq_obj.fit() np.random.seed(42) - res_manual = fit_pq(y, x, d, quantile, - clone(learner), clone(learner), - all_smpls, treatment, - n_rep=1, - trimming_threshold=trimming_threshold, - normalize_ipw=normalize_ipw) - - res_dict = {'coef': dml_pq_obj.coef.item(), - 'coef_manual': res_manual['pq'], - 'se': dml_pq_obj.se.item(), - 'se_manual': res_manual['se']} + res_manual = fit_pq( + y, + x, + d, + quantile, + clone(learner), + clone(learner), + all_smpls, + treatment, + n_rep=1, + trimming_threshold=trimming_threshold, + normalize_ipw=normalize_ipw, + ) + + res_dict = { + "coef": dml_pq_obj.coef.item(), + "coef_manual": res_manual["pq"], + "se": dml_pq_obj.se.item(), + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_pq_coef(dml_pq_fixture): - assert math.isclose(dml_pq_fixture['coef'], - dml_pq_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pq_fixture["coef"], dml_pq_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_pq_se(dml_pq_fixture): - assert math.isclose(dml_pq_fixture['se'], - dml_pq_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pq_fixture["se"], dml_pq_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/irm/tests/test_pq_external_predictions.py b/doubleml/irm/tests/test_pq_external_predictions.py index 87c0b03b8..28f8ec660 100644 --- a/doubleml/irm/tests/test_pq_external_predictions.py +++ b/doubleml/irm/tests/test_pq_external_predictions.py @@ -83,11 +83,7 @@ def doubleml_pq_fixture(n_rep, normalize_ipw, set_ml_m_ext, set_ml_g_ext): tol_rel = 1e-9 tol_abs = 1e-4 - res_dict = { - "coef_normal": dml_pq.coef.item(), - "coef_ext": dml_pq_ext.coef.item(), - "tol_rel": tol_rel, "tol_abs": - tol_abs} + res_dict = {"coef_normal": dml_pq.coef.item(), "coef_ext": dml_pq_ext.coef.item(), "tol_rel": tol_rel, "tol_abs": tol_abs} return res_dict diff --git a/doubleml/irm/tests/test_pq_tune.py b/doubleml/irm/tests/test_pq_tune.py index 90bd06b9b..322b93fd7 100644 --- a/doubleml/irm/tests/test_pq_tune.py +++ b/doubleml/irm/tests/test_pq_tune.py @@ -11,53 +11,45 @@ from ._utils_pq_manual import fit_pq, tune_nuisance_pq -@pytest.fixture(scope='module', - params=[0, 1]) +@pytest.fixture(scope="module", params=[0, 1]) def treatment(request): return request.param -@pytest.fixture(scope='module', - params=[0.25, 0.5, 0.75]) +@pytest.fixture(scope="module", params=[0.25, 0.5, 0.75]) def quantile(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestClassifier(max_depth=5, random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestClassifier(max_depth=5, random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestClassifier(max_depth=5, random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestClassifier(max_depth=5, random_state=42)]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestClassifier]: - par_grid = {'n_estimators': [5, 10, 15, 20]} + par_grid = {"n_estimators": [5, 10, 15, 20]} return par_grid -@pytest.fixture(scope='module') -def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner_g, learner_m, normalize_ipw, - tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} +@pytest.fixture(scope="module") +def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner_g, learner_m, normalize_ipw, tune_on_folds): + par_grid = {"ml_g": get_par_grid(learner_g), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 n_folds = 2 @@ -70,15 +62,18 @@ def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner_g, lear smpls = all_smpls[0] np.random.seed(42) - dml_pq_obj = dml.DoubleMLPQ(obj_dml_data, - clone(learner_g), clone(learner_m), - treatment=treatment, - quantile=quantile, - n_folds=n_folds, - n_rep=1, - normalize_ipw=normalize_ipw, - trimming_threshold=0.01, - draw_sample_splitting=False) + dml_pq_obj = dml.DoubleMLPQ( + obj_dml_data, + clone(learner_g), + clone(learner_m), + treatment=treatment, + quantile=quantile, + n_folds=n_folds, + n_rep=1, + normalize_ipw=normalize_ipw, + trimming_threshold=0.01, + draw_sample_splitting=False, + ) # synchronize the sample splitting dml_pq_obj.set_sample_splitting(all_smpls=all_smpls) @@ -92,47 +87,70 @@ def dml_pq_fixture(generate_data_quantiles, treatment, quantile, learner_g, lear np.random.seed(42) if tune_on_folds: - g_params, m_params = tune_nuisance_pq(y, x, d, - clone(learner_g), clone(learner_m), - smpls, treatment, quantile, - n_folds_tune, par_grid['ml_g'], par_grid['ml_m']) + g_params, m_params = tune_nuisance_pq( + y, + x, + d, + clone(learner_g), + clone(learner_m), + smpls, + treatment, + quantile, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + ) else: xx = [(np.arange(len(y)), np.array([]))] - g_params, m_params = tune_nuisance_pq(y, x, d, - clone(learner_g), clone(learner_m), - xx, treatment, quantile, - n_folds_tune, par_grid['ml_g'], par_grid['ml_m']) + g_params, m_params = tune_nuisance_pq( + y, + x, + d, + clone(learner_g), + clone(learner_m), + xx, + treatment, + quantile, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_m"], + ) g_params = g_params * n_folds m_params = m_params * n_folds np.random.seed(42) - res_manual = fit_pq(y, x, d, quantile, - learner_g=clone(learner_g), - learner_m=clone(learner_m), - all_smpls=all_smpls, - treatment=treatment, - n_rep=1, trimming_threshold=0.01, - normalize_ipw=normalize_ipw, - g_params=g_params, m_params=m_params) - - res_dict = {'coef': dml_pq_obj.coef.item(), - 'coef_manual': res_manual['pq'], - 'se': dml_pq_obj.se.item(), - 'se_manual': res_manual['se']} + res_manual = fit_pq( + y, + x, + d, + quantile, + learner_g=clone(learner_g), + learner_m=clone(learner_m), + all_smpls=all_smpls, + treatment=treatment, + n_rep=1, + trimming_threshold=0.01, + normalize_ipw=normalize_ipw, + g_params=g_params, + m_params=m_params, + ) + + res_dict = { + "coef": dml_pq_obj.coef.item(), + "coef_manual": res_manual["pq"], + "se": dml_pq_obj.se.item(), + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_pq_coef(dml_pq_fixture): - assert math.isclose(dml_pq_fixture['coef'], - dml_pq_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pq_fixture["coef"], dml_pq_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_pq_se(dml_pq_fixture): - assert math.isclose(dml_pq_fixture['se'], - dml_pq_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pq_fixture["se"], dml_pq_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/irm/tests/test_qte.py b/doubleml/irm/tests/test_qte.py index 08af9015a..0557c85b8 100644 --- a/doubleml/irm/tests/test_qte.py +++ b/doubleml/irm/tests/test_qte.py @@ -19,21 +19,19 @@ n_rep = 1 -@pytest.fixture(scope='module', - params=[RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), - LogisticRegression()]) +@pytest.fixture( + scope="module", params=[RandomForestClassifier(max_depth=2, n_estimators=10, random_state=42), LogisticRegression()] +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[None, _default_kde]) +@pytest.fixture(scope="module", params=[None, _default_kde]) def kde(request): return request.param @@ -41,7 +39,7 @@ def kde(request): @pytest.fixture(scope="module") def dml_qte_fixture(generate_data_quantiles, learner, normalize_ipw, kde): n_folds = 3 - boot_methods = ['normal'] + boot_methods = ["normal"] n_rep_boot = 2 # collect data @@ -58,26 +56,17 @@ def dml_qte_fixture(generate_data_quantiles, learner, normalize_ipw, kde): "n_rep": n_rep, "normalize_ipw": normalize_ipw, "trimming_threshold": 1e-12, - "kde": kde + "kde": kde, } np.random.seed(42) - dml_qte_obj = dml.DoubleMLQTE( - obj_dml_data, - ml_g, ml_m, - **input_args - ) + dml_qte_obj = dml.DoubleMLQTE(obj_dml_data, ml_g, ml_m, **input_args) unfitted_qte_model = copy.copy(dml_qte_obj) np.random.seed(42) dml_qte_obj.fit() np.random.seed(42) - dml_qte_obj_ext_smpls = dml.DoubleMLQTE( - obj_dml_data, - ml_g, ml_m, - draw_sample_splitting=False, - **input_args - ) + dml_qte_obj_ext_smpls = dml.DoubleMLQTE(obj_dml_data, ml_g, ml_m, draw_sample_splitting=False, **input_args) dml_qte_obj_ext_smpls.set_sample_splitting(dml_qte_obj.smpls) np.random.seed(42) dml_qte_obj_ext_smpls.fit() @@ -85,95 +74,103 @@ def dml_qte_fixture(generate_data_quantiles, learner, normalize_ipw, kde): np.random.seed(42) n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds, n_rep=1, groups=d) - res_manual = fit_qte(y, x, d, quantiles, ml_g, ml_g, all_smpls, - n_rep=n_rep, - normalize_ipw=normalize_ipw, - trimming_rule='truncate', trimming_threshold=1e-12, kde=kde, - draw_sample_splitting=True) + res_manual = fit_qte( + y, + x, + d, + quantiles, + ml_g, + ml_g, + all_smpls, + n_rep=n_rep, + normalize_ipw=normalize_ipw, + trimming_rule="truncate", + trimming_threshold=1e-12, + kde=kde, + draw_sample_splitting=True, + ) ci = dml_qte_obj.confint(joint=False, level=0.95) - ci_manual = confint_manual(res_manual['qte'], res_manual['se'], quantiles, - boot_t_stat=None, joint=False, level=0.95) - res_dict = {'coef': dml_qte_obj.coef, - 'coef_manual': res_manual['qte'], - 'coef_ext_smpls': dml_qte_obj_ext_smpls.coef, - 'se': dml_qte_obj.se, - 'se_manual': res_manual['se'], - 'se_ext_smpls': dml_qte_obj_ext_smpls.se, - 'boot_methods': boot_methods, - 'ci': ci.to_numpy(), - 'ci_manual': ci_manual.to_numpy(), - 'qte_model': dml_qte_obj, - 'unfitted_qte_model': unfitted_qte_model} + ci_manual = confint_manual(res_manual["qte"], res_manual["se"], quantiles, boot_t_stat=None, joint=False, level=0.95) + res_dict = { + "coef": dml_qte_obj.coef, + "coef_manual": res_manual["qte"], + "coef_ext_smpls": dml_qte_obj_ext_smpls.coef, + "se": dml_qte_obj.se, + "se_manual": res_manual["se"], + "se_ext_smpls": dml_qte_obj_ext_smpls.se, + "boot_methods": boot_methods, + "ci": ci.to_numpy(), + "ci_manual": ci_manual.to_numpy(), + "qte_model": dml_qte_obj, + "unfitted_qte_model": unfitted_qte_model, + } for bootstrap in boot_methods: np.random.seed(42) - boot_t_stat = boot_qte(res_manual['scaled_scores'], res_manual['ses'], quantiles, - all_smpls, n_rep, bootstrap, n_rep_boot) + boot_t_stat = boot_qte( + res_manual["scaled_scores"], res_manual["ses"], quantiles, all_smpls, n_rep, bootstrap, n_rep_boot + ) np.random.seed(42) dml_qte_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat_' + bootstrap] = dml_qte_obj.boot_t_stat - res_dict['boot_t_stat_' + bootstrap + '_manual'] = boot_t_stat + res_dict["boot_t_stat_" + bootstrap] = dml_qte_obj.boot_t_stat + res_dict["boot_t_stat_" + bootstrap + "_manual"] = boot_t_stat ci = dml_qte_obj.confint(joint=True, level=0.95) - ci_manual = confint_manual(res_manual['qte'], res_manual['se'], quantiles, - boot_t_stat=boot_t_stat, joint=True, level=0.95) - res_dict['boot_ci_' + bootstrap] = ci.to_numpy() - res_dict['boot_ci_' + bootstrap + '_manual'] = ci_manual.to_numpy() + ci_manual = confint_manual( + res_manual["qte"], res_manual["se"], quantiles, boot_t_stat=boot_t_stat, joint=True, level=0.95 + ) + res_dict["boot_ci_" + bootstrap] = ci.to_numpy() + res_dict["boot_ci_" + bootstrap + "_manual"] = ci_manual.to_numpy() return res_dict @pytest.mark.ci def test_dml_qte_coef(dml_qte_fixture): - assert np.allclose(dml_qte_fixture['coef'], - dml_qte_fixture['coef_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_qte_fixture['coef'], - dml_qte_fixture['coef_ext_smpls'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_qte_fixture["coef"], dml_qte_fixture["coef_manual"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_qte_fixture["coef"], dml_qte_fixture["coef_ext_smpls"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_qte_se(dml_qte_fixture): - assert np.allclose(dml_qte_fixture['se'], - dml_qte_fixture['se_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_qte_fixture['se'], - dml_qte_fixture['se_ext_smpls'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_qte_fixture["se"], dml_qte_fixture["se_manual"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_qte_fixture["se"], dml_qte_fixture["se_ext_smpls"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_qte_boot(dml_qte_fixture): - for bootstrap in dml_qte_fixture['boot_methods']: - assert np.allclose(dml_qte_fixture['boot_t_stat_' + bootstrap], - dml_qte_fixture['boot_t_stat_' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_qte_fixture["boot_methods"]: + assert np.allclose( + dml_qte_fixture["boot_t_stat_" + bootstrap], + dml_qte_fixture["boot_t_stat_" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_qte_ci(dml_qte_fixture): - assert np.allclose(dml_qte_fixture['ci'], - dml_qte_fixture['ci_manual'], - rtol=1e-9, atol=1e-4) - for bootstrap in dml_qte_fixture['boot_methods']: - assert np.allclose(dml_qte_fixture['boot_ci_' + bootstrap], - dml_qte_fixture['boot_ci_' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_qte_fixture["ci"], dml_qte_fixture["ci_manual"], rtol=1e-9, atol=1e-4) + for bootstrap in dml_qte_fixture["boot_methods"]: + assert np.allclose( + dml_qte_fixture["boot_ci_" + bootstrap], dml_qte_fixture["boot_ci_" + bootstrap + "_manual"], rtol=1e-9, atol=1e-4 + ) @pytest.mark.ci def test_doubleml_qte_exceptions(): np.random.seed(42) - (x, y, d) = make_irm_data(1000, 5, 2, return_type='array') + (x, y, d) = make_irm_data(1000, 5, 2, return_type="array") obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d) ml_g = RandomForestClassifier(n_estimators=20) ml_m = RandomForestClassifier(n_estimators=20) - msg = ('Sample splitting not specified. ' - r'Either draw samples via .draw_sample splitting\(\) or set external samples via .set_sample_splitting\(\).') + msg = ( + "Sample splitting not specified. " + r"Either draw samples via .draw_sample splitting\(\) or set external samples via .set_sample_splitting\(\)." + ) with pytest.raises(ValueError, match=msg): dml_obj = dml.DoubleMLQTE(obj_dml_data, ml_g, ml_m, draw_sample_splitting=False) _ = dml_obj.smpls @@ -181,8 +178,8 @@ def test_doubleml_qte_exceptions(): @pytest.mark.ci def test_doubleml_qte_return_types(dml_qte_fixture): - assert isinstance(dml_qte_fixture['qte_model'].__str__(), str) - assert isinstance(dml_qte_fixture['qte_model'].summary, pd.DataFrame) + assert isinstance(dml_qte_fixture["qte_model"].__str__(), str) + assert isinstance(dml_qte_fixture["qte_model"].summary, pd.DataFrame) - assert dml_qte_fixture['qte_model'].all_coef.shape == (n_quantiles, n_rep) - assert isinstance(dml_qte_fixture['unfitted_qte_model'].summary, pd.DataFrame) + assert dml_qte_fixture["qte_model"].all_coef.shape == (n_quantiles, n_rep) + assert isinstance(dml_qte_fixture["unfitted_qte_model"].summary, pd.DataFrame) diff --git a/doubleml/irm/tests/test_qte_exceptions.py b/doubleml/irm/tests/test_qte_exceptions.py index 8a8fa6459..32193c30a 100644 --- a/doubleml/irm/tests/test_qte_exceptions.py +++ b/doubleml/irm/tests/test_qte_exceptions.py @@ -16,8 +16,7 @@ class DummyDataClass(DoubleMLBaseData): - def __init__(self, - data): + def __init__(self, data): DoubleMLBaseData.__init__(self, data) @property @@ -27,56 +26,58 @@ def n_coefs(self): @pytest.mark.ci def test_exception_data(): - msg = 'The data must be of DoubleMLData type.' + msg = "The data must be of DoubleMLData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLQTE(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_g, ml_m) - msg = ('Incompatible data. To fit an PQ model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an PQ model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_irm = dml_data_irm.data.copy() - df_irm['d'] = df_irm['d'] * 2 + df_irm["d"] = df_irm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for QTE - _ = DoubleMLQTE(DoubleMLData(df_irm, 'y', 'd'), - LogisticRegression(), LogisticRegression()) + _ = DoubleMLQTE(DoubleMLData(df_irm, "y", "d"), LogisticRegression(), LogisticRegression()) df_irm = dml_data_irm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for QTE - _ = DoubleMLQTE(DoubleMLData(df_irm, 'y', ['d', 'X1']), - LogisticRegression(), LogisticRegression()) + _ = DoubleMLQTE(DoubleMLData(df_irm, "y", ["d", "X1"]), LogisticRegression(), LogisticRegression()) @pytest.mark.ci def test_exception_score(): # QTE - msg = 'Invalid score IV. Valid score PQ or LPQ or CVaR.' + msg = "Invalid score IV. Valid score PQ or LPQ or CVaR." with pytest.raises(ValueError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), score=2) @pytest.mark.ci def test_exception_trimming_rule(): - msg = 'Invalid trimming_rule discard. Valid trimming_rule truncate.' + msg = "Invalid trimming_rule discard. Valid trimming_rule truncate." with pytest.raises(ValueError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule='discard') + _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule="discard") msg = "trimming_threshold has to be a float. Object of type passed." with pytest.raises(TypeError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold="0.1") + _ = DoubleMLQTE( + dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule="truncate", trimming_threshold="0.1" + ) - msg = 'Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5.' + msg = "Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5." with pytest.raises(ValueError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold=0.6) + _ = DoubleMLQTE( + dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule="truncate", trimming_threshold=0.6 + ) @pytest.mark.ci def test_exception_quantiles(): - msg = r'Quantiles have be between 0 or 1. Quantiles \[0.2 2. \] passed.' + msg = r"Quantiles have be between 0 or 1. Quantiles \[0.2 2. \] passed." with pytest.raises(ValueError, match=msg): _ = DoubleMLQTE(dml_data_irm, ml_g, ml_m, quantiles=[0.2, 2]) @@ -91,18 +92,18 @@ def test_exception_ipw_normalization(): @pytest.mark.ci def test_exception_bootstrap(): dml_qte_boot = DoubleMLQTE(dml_data_irm, RandomForestClassifier(), RandomForestClassifier()) - msg = r'Apply fit\(\) before bootstrap\(\).' + msg = r"Apply fit\(\) before bootstrap\(\)." with pytest.raises(ValueError, match=msg): dml_qte_boot.bootstrap() dml_qte_boot.fit() msg = 'Method must be "Bayes", "normal" or "wild". Got Gaussian.' with pytest.raises(ValueError, match=msg): - dml_qte_boot.bootstrap(method='Gaussian') + dml_qte_boot.bootstrap(method="Gaussian") msg = "The number of bootstrap replications must be of int type. 500 of type was passed." with pytest.raises(TypeError, match=msg): - dml_qte_boot.bootstrap(n_rep_boot='500') - msg = 'The number of bootstrap replications must be positive. 0 was passed.' + dml_qte_boot.bootstrap(n_rep_boot="500") + msg = "The number of bootstrap replications must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): dml_qte_boot.bootstrap(n_rep_boot=0) @@ -112,21 +113,21 @@ def test_doubleml_exception_confint(): dml_qte_confint = DoubleMLQTE(dml_data_irm, RandomForestClassifier(), RandomForestClassifier()) dml_qte_confint.fit() - msg = 'joint must be True or False. Got 1.' + msg = "joint must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_qte_confint.confint(joint=1) msg = "The confidence level must be of float type. 5% of type was passed." with pytest.raises(TypeError, match=msg): - dml_qte_confint.confint(level='5%') - msg = r'The confidence level must be in \(0,1\). 0.0 was passed.' + dml_qte_confint.confint(level="5%") + msg = r"The confidence level must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): - dml_qte_confint.confint(level=0.) + dml_qte_confint.confint(level=0.0) dml_qte_confint_not_fitted = DoubleMLQTE(dml_data_irm, RandomForestClassifier(), RandomForestClassifier()) - msg = r'Apply fit\(\) before confint\(\).' + msg = r"Apply fit\(\) before confint\(\)." with pytest.raises(ValueError, match=msg): dml_qte_confint_not_fitted.confint() - msg = r'Apply bootstrap\(\) before confint\(joint=True\).' + msg = r"Apply bootstrap\(\) before confint\(joint=True\)." with pytest.raises(ValueError, match=msg): dml_qte_confint.confint(joint=True) dml_qte_confint.bootstrap() @@ -138,15 +139,15 @@ def test_doubleml_exception_confint(): def test_doubleml_exception_p_adjust(): dml_qte_p_adjust = DoubleMLQTE(dml_data_irm, RandomForestClassifier(), RandomForestClassifier()) - msg = r'Apply fit\(\) before p_adjust\(\).' + msg = r"Apply fit\(\) before p_adjust\(\)." with pytest.raises(ValueError, match=msg): dml_qte_p_adjust.p_adjust() dml_qte_p_adjust.fit() msg = r'Apply bootstrap\(\) before p_adjust\("romano-wolf"\).' with pytest.raises(ValueError, match=msg): - dml_qte_p_adjust.p_adjust(method='romano-wolf') + dml_qte_p_adjust.p_adjust(method="romano-wolf") dml_qte_p_adjust.bootstrap() - p_val = dml_qte_p_adjust.p_adjust(method='romano-wolf') + p_val = dml_qte_p_adjust.p_adjust(method="romano-wolf") assert isinstance(p_val, pd.DataFrame) msg = "The p_adjust method must be of str type. 0.05 of type was passed." diff --git a/doubleml/irm/tests/test_ssm.py b/doubleml/irm/tests/test_ssm.py index 5e8728383..b157794b8 100644 --- a/doubleml/irm/tests/test_ssm.py +++ b/doubleml/irm/tests/test_ssm.py @@ -11,40 +11,35 @@ from ._utils_ssm_manual import fit_selection -@pytest.fixture(scope='module', - params=[[LassoCV(), - LogisticRegressionCV(penalty='l1', solver='liblinear')]]) +@pytest.fixture(scope="module", params=[[LassoCV(), LogisticRegressionCV(penalty="l1", solver="liblinear")]]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['missing-at-random', 'nonignorable']) +@pytest.fixture(scope="module", params=["missing-at-random", "nonignorable"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[0.01]) +@pytest.fixture(scope="module", params=[0.01]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') -def dml_selection_fixture(generate_data_selection_mar, generate_data_selection_nonignorable, - learner, score, - trimming_threshold, normalize_ipw): +@pytest.fixture(scope="module") +def dml_selection_fixture( + generate_data_selection_mar, generate_data_selection_nonignorable, learner, score, trimming_threshold, normalize_ipw +): n_folds = 3 # collect data np.random.seed(42) - if score == 'missing-at-random': + if score == "missing-at-random": (x, y, d, z, s) = generate_data_selection_mar else: (x, y, d, z, s) = generate_data_selection_nonignorable @@ -58,36 +53,41 @@ def dml_selection_fixture(generate_data_selection_mar, generate_data_selection_n all_smpls = draw_smpls(n_obs, n_folds) np.random.seed(42) - if score == 'missing-at-random': + if score == "missing-at-random": obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, z=None, s=s) - dml_sel_obj = dml.DoubleMLSSM(obj_dml_data, - ml_g, ml_pi, ml_m, - n_folds=n_folds, - score=score) + dml_sel_obj = dml.DoubleMLSSM(obj_dml_data, ml_g, ml_pi, ml_m, n_folds=n_folds, score=score) else: - assert score == 'nonignorable' + assert score == "nonignorable" obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, z=z, s=s) - dml_sel_obj = dml.DoubleMLSSM(obj_dml_data, - ml_g, ml_pi, ml_m, - n_folds=n_folds, - score=score) + dml_sel_obj = dml.DoubleMLSSM(obj_dml_data, ml_g, ml_pi, ml_m, n_folds=n_folds, score=score) np.random.seed(42) dml_sel_obj.set_sample_splitting(all_smpls=all_smpls) dml_sel_obj.fit() np.random.seed(42) - res_manual = fit_selection(y, x, d, z, s, - clone(learner[0]), clone(learner[1]), clone(learner[1]), - all_smpls, score, - trimming_rule='truncate', - trimming_threshold=trimming_threshold, - normalize_ipw=normalize_ipw) - - res_dict = {'coef': dml_sel_obj.coef[0], - 'coef_manual': res_manual['theta'], - 'se': dml_sel_obj.se[0], - 'se_manual': res_manual['se']} + res_manual = fit_selection( + y, + x, + d, + z, + s, + clone(learner[0]), + clone(learner[1]), + clone(learner[1]), + all_smpls, + score, + trimming_rule="truncate", + trimming_threshold=trimming_threshold, + normalize_ipw=normalize_ipw, + ) + + res_dict = { + "coef": dml_sel_obj.coef[0], + "coef_manual": res_manual["theta"], + "se": dml_sel_obj.se[0], + "se_manual": res_manual["se"], + } # sensitivity tests # TODO @@ -97,13 +97,9 @@ def dml_selection_fixture(generate_data_selection_mar, generate_data_selection_n @pytest.mark.ci def test_dml_selection_coef(dml_selection_fixture): - assert math.isclose(dml_selection_fixture['coef'], - dml_selection_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-2) + assert math.isclose(dml_selection_fixture["coef"], dml_selection_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-2) @pytest.mark.ci def test_dml_selection_se(dml_selection_fixture): - assert math.isclose(dml_selection_fixture['se'], - dml_selection_fixture['se_manual'], - rel_tol=1e-9, abs_tol=5e-2) + assert math.isclose(dml_selection_fixture["se"], dml_selection_fixture["se_manual"], rel_tol=1e-9, abs_tol=5e-2) diff --git a/doubleml/irm/tests/test_ssm_exceptions.py b/doubleml/irm/tests/test_ssm_exceptions.py index bc6c0504a..f3eebd19d 100644 --- a/doubleml/irm/tests/test_ssm_exceptions.py +++ b/doubleml/irm/tests/test_ssm_exceptions.py @@ -16,12 +16,11 @@ ml_pi = LogisticRegression() ml_m = LogisticRegression() dml_ssm_mar = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m) -dml_ssm_nonignorable = DoubleMLSSM(dml_data_nonignorable, ml_g, ml_pi, ml_m, score='nonignorable') +dml_ssm_nonignorable = DoubleMLSSM(dml_data_nonignorable, ml_g, ml_pi, ml_m, score="nonignorable") class DummyDataClass(DoubleMLBaseData): - def __init__(self, - data): + def __init__(self, data): DoubleMLBaseData.__init__(self, data) @property @@ -31,49 +30,47 @@ def n_coefs(self): @pytest.mark.ci def test_ssm_exception_data(): - msg = 'The data must be of DoubleMLData or DoubleMLClusterData type.' + msg = "The data must be of DoubleMLData or DoubleMLClusterData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(pd.DataFrame(), ml_g, ml_pi, ml_m) - msg = 'The data must be of DoubleMLData type.' + msg = "The data must be of DoubleMLData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_g, ml_pi, ml_m) # Nonignorable nonresponse without instrument - msg = ('Sample selection by nonignorable nonresponse was set but instrumental variable \ + msg = "Sample selection by nonignorable nonresponse was set but instrumental variable \ is None. To estimate treatment effect under nonignorable nonresponse, \ - specify an instrument for the selection variable.') + specify an instrument for the selection variable." with pytest.raises(ValueError, match=msg): - _ = DoubleMLSSM(dml_data_mar, Lasso(), LogisticRegression(), LogisticRegression(), score='nonignorable') + _ = DoubleMLSSM(dml_data_mar, Lasso(), LogisticRegression(), LogisticRegression(), score="nonignorable") @pytest.mark.ci def test_ssm_exception_scores(): # MAR - msg = 'Invalid score MAR. Valid score missing-at-random or nonignorable.' + msg = "Invalid score MAR. Valid score missing-at-random or nonignorable." with pytest.raises(ValueError, match=msg): - _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, score='MAR') - msg = 'score should be either a string or a callable. 0 was passed.' + _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, score="MAR") + msg = "score should be either a string or a callable. 0 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, score=0) @pytest.mark.ci def test_ssm_exception_trimming_rule(): - msg = 'Invalid trimming_rule discard. Valid trimming_rule truncate.' + msg = "Invalid trimming_rule discard. Valid trimming_rule truncate." with pytest.raises(ValueError, match=msg): - _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, trimming_rule='discard') + _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, trimming_rule="discard") # check the trimming_threshold exceptions msg = "trimming_threshold has to be a float. Object of type passed." with pytest.raises(TypeError, match=msg): - _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, - trimming_rule='truncate', trimming_threshold="0.1") + _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, trimming_rule="truncate", trimming_threshold="0.1") - msg = 'Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5.' + msg = "Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5." with pytest.raises(ValueError, match=msg): - _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, - trimming_rule='truncate', trimming_threshold=0.6) + _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, trimming_rule="truncate", trimming_threshold=0.6) @pytest.mark.ci @@ -88,32 +85,33 @@ def test_ssm_exception_resampling(): msg = "The number of folds must be of int type. 1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, n_folds=1.5) - msg = ('The number of repetitions for the sample splitting must be of int type. ' - "1.5 of type was passed.") + msg = "The number of repetitions for the sample splitting must be of int type. " "1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, n_rep=1.5) - msg = 'The number of folds must be positive. 0 was passed.' + msg = "The number of folds must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, n_folds=0) - msg = 'The number of repetitions for the sample splitting must be positive. 0 was passed.' + msg = "The number of repetitions for the sample splitting must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, n_rep=0) - msg = 'draw_sample_splitting must be True or False. Got true.' + msg = "draw_sample_splitting must be True or False. Got true." with pytest.raises(TypeError, match=msg): - _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, draw_sample_splitting='true') + _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, draw_sample_splitting="true") @pytest.mark.ci def test_ssm_exception_get_params(): - msg = 'Invalid nuisance learner ml_r. Valid nuisance learner ml_g_d0 or ml_g_d1 or ml_pi or ml_m.' + msg = "Invalid nuisance learner ml_r. Valid nuisance learner ml_g_d0 or ml_g_d1 or ml_pi or ml_m." with pytest.raises(ValueError, match=msg): - dml_ssm_mar.get_params('ml_r') + dml_ssm_mar.get_params("ml_r") @pytest.mark.ci def test_ssm_exception_smpls(): - msg = ('Sample splitting not specified. ' - r'Either draw samples via .draw_sample splitting\(\) or set external samples via .set_sample_splitting\(\).') + msg = ( + "Sample splitting not specified. " + r"Either draw samples via .draw_sample splitting\(\) or set external samples via .set_sample_splitting\(\)." + ) dml_plr_no_smpls = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, draw_sample_splitting=False) with pytest.raises(ValueError, match=msg): _ = dml_plr_no_smpls.smpls @@ -123,11 +121,11 @@ def test_ssm_exception_smpls(): def test_ssm_exception_fit(): msg = "The number of CPUs used to fit the learners must be of int type. 5 of type was passed." with pytest.raises(TypeError, match=msg): - dml_ssm_mar.fit(n_jobs_cv='5') - msg = 'store_predictions must be True or False. Got 1.' + dml_ssm_mar.fit(n_jobs_cv="5") + msg = "store_predictions must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_ssm_mar.fit(store_predictions=1) - msg = 'store_models must be True or False. Got 1.' + msg = "store_models must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_ssm_mar.fit(store_models=1) @@ -135,18 +133,18 @@ def test_ssm_exception_fit(): @pytest.mark.ci def test_ssm_exception_bootstrap(): dml_ssm_boot = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m) - msg = r'Apply fit\(\) before bootstrap\(\).' + msg = r"Apply fit\(\) before bootstrap\(\)." with pytest.raises(ValueError, match=msg): dml_ssm_boot.bootstrap() dml_ssm_boot.fit() msg = 'Method must be "Bayes", "normal" or "wild". Got Gaussian.' with pytest.raises(ValueError, match=msg): - dml_ssm_boot.bootstrap(method='Gaussian') + dml_ssm_boot.bootstrap(method="Gaussian") msg = "The number of bootstrap replications must be of int type. 500 of type was passed." with pytest.raises(TypeError, match=msg): - dml_ssm_boot.bootstrap(n_rep_boot='500') - msg = 'The number of bootstrap replications must be positive. 0 was passed.' + dml_ssm_boot.bootstrap(n_rep_boot="500") + msg = "The number of bootstrap replications must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): dml_ssm_boot.bootstrap(n_rep_boot=0) @@ -154,22 +152,22 @@ def test_ssm_exception_bootstrap(): @pytest.mark.ci def test_ssm_exception_confint(): dml_ssm_confint = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m) - msg = r'Apply fit\(\) before confint\(\).' + msg = r"Apply fit\(\) before confint\(\)." with pytest.raises(ValueError, match=msg): dml_ssm_confint.confint() dml_ssm_confint.fit() - msg = 'joint must be True or False. Got 1.' + msg = "joint must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_ssm_confint.confint(joint=1) msg = "The confidence level must be of float type. 5% of type was passed." with pytest.raises(TypeError, match=msg): - dml_ssm_confint.confint(level='5%') - msg = r'The confidence level must be in \(0,1\). 0.0 was passed.' + dml_ssm_confint.confint(level="5%") + msg = r"The confidence level must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): - dml_ssm_confint.confint(level=0.) + dml_ssm_confint.confint(level=0.0) - msg = r'Apply bootstrap\(\) before confint\(joint=True\).' + msg = r"Apply bootstrap\(\) before confint\(joint=True\)." with pytest.raises(ValueError, match=msg): dml_ssm_confint.confint(joint=True) dml_ssm_confint.bootstrap() @@ -179,12 +177,12 @@ def test_ssm_exception_confint(): @pytest.mark.ci def test_ssm_exception_set_ml_nuisance_params(): - msg = 'Invalid nuisance learner g. Valid nuisance learner ml_g_d0 or ml_g_d1 or ml_pi or ml_m.' + msg = "Invalid nuisance learner g. Valid nuisance learner ml_g_d0 or ml_g_d1 or ml_pi or ml_m." with pytest.raises(ValueError, match=msg): - dml_ssm_mar.set_ml_nuisance_params('g', 'd', {'alpha': 0.1}) - msg = 'Invalid treatment variable y. Valid treatment variable d.' + dml_ssm_mar.set_ml_nuisance_params("g", "d", {"alpha": 0.1}) + msg = "Invalid treatment variable y. Valid treatment variable d." with pytest.raises(ValueError, match=msg): - dml_ssm_mar.set_ml_nuisance_params('ml_g_d0', 'y', {'alpha': 0.1}) + dml_ssm_mar.set_ml_nuisance_params("ml_g_d0", "y", {"alpha": 0.1}) class _DummyNoSetParams: @@ -210,24 +208,26 @@ def predict_proba(self): r"ignore:.*is \(probably\) neither a regressor nor a classifier.*:UserWarning", ) def test_ssm_exception_learner(): - err_msg_prefix = 'Invalid learner provided for ml_g: ' + err_msg_prefix = "Invalid learner provided for ml_g: " - msg = err_msg_prefix + 'provide an instance of a learner instead of a class.' + msg = err_msg_prefix + "provide an instance of a learner instead of a class." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, Lasso, ml_pi, ml_m) - msg = err_msg_prefix + r'BaseEstimator\(\) has no method .fit\(\).' + msg = err_msg_prefix + r"BaseEstimator\(\) has no method .fit\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, BaseEstimator(), ml_pi, ml_m) - msg = r'has no method .set_params\(\).' + msg = r"has no method .set_params\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, _DummyNoSetParams(), ml_pi, ml_m) - msg = r'has no method .get_params\(\).' + msg = r"has no method .get_params\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, _DummyNoGetParams(), ml_pi, ml_m) # allow classifiers for ml_g, but only for binary outcome - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier ' - 'but the outcome is not binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier " + "but the outcome is not binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLSSM(dml_data_mar, LogisticRegression(), ml_pi, ml_m) @@ -235,7 +235,7 @@ def test_ssm_exception_learner(): # it then predicts labels and therefore an exception will be thrown log_reg = LogisticRegression() log_reg._estimator_type = None - msg = (r'Learner provided for ml_m is probably invalid: LogisticRegression\(\) is \(probably\) no classifier.') + msg = r"Learner provided for ml_m is probably invalid: LogisticRegression\(\) is \(probably\) no classifier." with pytest.warns(UserWarning, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, log_reg) @@ -244,16 +244,16 @@ def test_ssm_exception_learner(): @pytest.mark.filterwarnings( r"ignore:.*is \(probably\) neither a regressor nor a classifier.*:UserWarning", r"ignore: Learner provided for ml_m is probably invalid.*is \(probably\) no classifier.*:UserWarning", - r"ignore: Learner provided for l_pi is probably invalid.*is \(probably\) no classifier.*:UserWarning" + r"ignore: Learner provided for ml_pi is probably invalid.*is \(probably\) no classifier.*:UserWarning", ) def test_ssm_exception_and_warning_learner(): # msg = err_msg_prefix + r'_DummyNoClassifier\(\) has no method .predict\(\).' with pytest.raises(TypeError): _ = DoubleMLSSM(dml_data_mar, _DummyNoClassifier(), ml_pi, ml_m) - msg = 'Invalid learner provided for ml_pi: ' + r'Lasso\(\) has no method .predict_proba\(\).' + msg = "Invalid learner provided for ml_pi: " + r"Lasso\(\) has no method .predict_proba\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, Lasso(), Lasso(), ml_m) - msg = 'Invalid learner provided for ml_m: ' + r'Lasso\(\) has no method .predict_proba\(\).' + msg = "Invalid learner provided for ml_m: " + r"Lasso\(\) has no method .predict_proba\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, Lasso(), ml_pi, Lasso()) @@ -276,25 +276,27 @@ def predict(self, X): @pytest.mark.ci def test_ssm_nan_prediction(): - msg = r'Predictions from learner LassoWithNanPred\(\) for ml_g_d1 are not finite.' + msg = r"Predictions from learner LassoWithNanPred\(\) for ml_g_d1 are not finite." with pytest.raises(ValueError, match=msg): _ = DoubleMLSSM(dml_data_mar, LassoWithNanPred(), ml_pi, ml_m).fit() - msg = r'Predictions from learner LassoWithInfPred\(\) for ml_g_d1 are not finite.' + msg = r"Predictions from learner LassoWithInfPred\(\) for ml_g_d1 are not finite." with pytest.raises(ValueError, match=msg): _ = DoubleMLSSM(dml_data_mar, LassoWithInfPred(), ml_pi, ml_m).fit() @pytest.mark.ci def test_double_ml_exception_evaluate_learner(): - dml_ssm_obj = DoubleMLSSM(dml_data_mar, - ml_g=Lasso(), - ml_pi=LogisticRegression(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='missing-at-random') - - msg = r'Apply fit\(\) before evaluate_learners\(\).' + dml_ssm_obj = DoubleMLSSM( + dml_data_mar, + ml_g=Lasso(), + ml_pi=LogisticRegression(), + ml_m=LogisticRegression(), + trimming_threshold=0.05, + n_folds=5, + score="missing-at-random", + ) + + msg = r"Apply fit\(\) before evaluate_learners\(\)." with pytest.raises(ValueError, match=msg): dml_ssm_obj.evaluate_learners() @@ -304,14 +306,17 @@ def test_double_ml_exception_evaluate_learner(): with pytest.raises(TypeError, match=msg): dml_ssm_obj.evaluate_learners(metric="mse") - msg = (r"The learners have to be a subset of \['ml_g_d0', 'ml_g_d1', 'ml_pi', 'ml_m'\]. " - r"Learners \['ml_mu', 'ml_p'\] provided.") + msg = ( + r"The learners have to be a subset of \['ml_g_d0', 'ml_g_d1', 'ml_pi', 'ml_m'\]. " + r"Learners \['ml_mu', 'ml_p'\] provided." + ) with pytest.raises(ValueError, match=msg): - dml_ssm_obj.evaluate_learners(learners=['ml_mu', 'ml_p']) + dml_ssm_obj.evaluate_learners(learners=["ml_mu", "ml_p"]) - msg = 'Evaluation from learner ml_g_d0 is not finite.' + msg = "Evaluation from learner ml_g_d0 is not finite." def eval_fct(y_pred, y_true): return np.nan + with pytest.raises(ValueError, match=msg): dml_ssm_obj.evaluate_learners(metric=eval_fct) diff --git a/doubleml/irm/tests/test_ssm_tune.py b/doubleml/irm/tests/test_ssm_tune.py index 9e6170c14..0fafbc134 100644 --- a/doubleml/irm/tests/test_ssm_tune.py +++ b/doubleml/irm/tests/test_ssm_tune.py @@ -12,58 +12,57 @@ from ._utils_ssm_manual import fit_selection, tune_nuisance_ssm -@pytest.fixture(scope='module', - params=[RandomForestRegressor(random_state=42)]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(random_state=42)]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression(random_state=42)]) +@pytest.fixture(scope="module", params=[LogisticRegression(random_state=42)]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=['missing-at-random', 'nonignorable']) +@pytest.fixture(scope="module", params=["missing-at-random", "nonignorable"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def normalize_ipw(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ in [RandomForestRegressor]: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ in [LogisticRegression] - par_grid = {'C': np.logspace(-2, 2, 10)} + par_grid = {"C": np.logspace(-2, 2, 10)} return par_grid -@pytest.fixture(scope='module') -def dml_ssm_fixture(generate_data_selection_mar, generate_data_selection_nonignorable, - learner_g, learner_m, score, - normalize_ipw, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_pi': get_par_grid(learner_m), - 'ml_m': get_par_grid(learner_m)} +@pytest.fixture(scope="module") +def dml_ssm_fixture( + generate_data_selection_mar, + generate_data_selection_nonignorable, + learner_g, + learner_m, + score, + normalize_ipw, + tune_on_folds, +): + par_grid = {"ml_g": get_par_grid(learner_g), "ml_pi": get_par_grid(learner_m), "ml_m": get_par_grid(learner_m)} n_folds_tune = 4 n_folds = 2 # collect data np.random.seed(42) - if score == 'missing-at-random': + if score == "missing-at-random": (x, y, d, z, s) = generate_data_selection_mar else: (x, y, d, z, s) = generate_data_selection_nonignorable @@ -76,23 +75,31 @@ def dml_ssm_fixture(generate_data_selection_mar, generate_data_selection_nonigno ml_m = clone(learner_m) np.random.seed(42) - if score == 'missing-at-random': + if score == "missing-at-random": obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, z=None, s=s) - dml_sel_obj = dml.DoubleMLSSM(obj_dml_data, - ml_g, ml_pi, ml_m, - n_folds=n_folds, - score=score, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) + dml_sel_obj = dml.DoubleMLSSM( + obj_dml_data, + ml_g, + ml_pi, + ml_m, + n_folds=n_folds, + score=score, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + ) else: - assert score == 'nonignorable' + assert score == "nonignorable" obj_dml_data = dml.DoubleMLData.from_arrays(x, y, d, z=z, s=s) - dml_sel_obj = dml.DoubleMLSSM(obj_dml_data, - ml_g, ml_pi, ml_m, - n_folds=n_folds, - score=score, - normalize_ipw=normalize_ipw, - draw_sample_splitting=False) + dml_sel_obj = dml.DoubleMLSSM( + obj_dml_data, + ml_g, + ml_pi, + ml_m, + n_folds=n_folds, + score=score, + normalize_ipw=normalize_ipw, + draw_sample_splitting=False, + ) # synchronize the sample splitting np.random.seed(42) @@ -100,8 +107,7 @@ def dml_ssm_fixture(generate_data_selection_mar, generate_data_selection_nonigno np.random.seed(42) # tune hyperparameters - tune_res = dml_sel_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_sel_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLSSM) dml_sel_obj.fit() @@ -110,18 +116,40 @@ def dml_ssm_fixture(generate_data_selection_mar, generate_data_selection_nonigno smpls = all_smpls[0] if tune_on_folds: g0_best_params, g1_best_params, pi_best_params, m_best_params = tune_nuisance_ssm( - y, x, d, z, s, - clone(learner_g), clone(learner_m), clone(learner_m), - smpls, score, n_folds_tune, - par_grid['ml_g'], par_grid['ml_pi'], par_grid['ml_m']) + y, + x, + d, + z, + s, + clone(learner_g), + clone(learner_m), + clone(learner_m), + smpls, + score, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_pi"], + par_grid["ml_m"], + ) else: xx = [(np.arange(len(y)), np.array([]))] g0_best_params, g1_best_params, pi_best_params, m_best_params = tune_nuisance_ssm( - y, x, d, z, s, - clone(learner_g), clone(learner_m), clone(learner_m), - xx, score, n_folds_tune, - par_grid['ml_g'], par_grid['ml_pi'], par_grid['ml_m']) + y, + x, + d, + z, + s, + clone(learner_g), + clone(learner_m), + clone(learner_m), + xx, + score, + n_folds_tune, + par_grid["ml_g"], + par_grid["ml_pi"], + par_grid["ml_m"], + ) g0_best_params = g0_best_params * n_folds g1_best_params = g1_best_params * n_folds @@ -129,30 +157,39 @@ def dml_ssm_fixture(generate_data_selection_mar, generate_data_selection_nonigno m_best_params = m_best_params * n_folds np.random.seed(42) - res_manual = fit_selection(y, x, d, z, s, - clone(learner_g), clone(learner_m), clone(learner_m), - all_smpls, score, - normalize_ipw=normalize_ipw, - g_d0_params=g0_best_params, g_d1_params=g1_best_params, - pi_params=pi_best_params, m_params=m_best_params) - - res_dict = {'coef': dml_sel_obj.coef[0], - 'coef_manual': res_manual['theta'], - 'se': dml_sel_obj.se[0], - 'se_manual': res_manual['se']} + res_manual = fit_selection( + y, + x, + d, + z, + s, + clone(learner_g), + clone(learner_m), + clone(learner_m), + all_smpls, + score, + normalize_ipw=normalize_ipw, + g_d0_params=g0_best_params, + g_d1_params=g1_best_params, + pi_params=pi_best_params, + m_params=m_best_params, + ) + + res_dict = { + "coef": dml_sel_obj.coef[0], + "coef_manual": res_manual["theta"], + "se": dml_sel_obj.se[0], + "se_manual": res_manual["se"], + } return res_dict @pytest.mark.ci def test_dml_ssm_coef(dml_ssm_fixture): - assert math.isclose(dml_ssm_fixture['coef'], - dml_ssm_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_ssm_fixture["coef"], dml_ssm_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_ssm_se(dml_ssm_fixture): - assert math.isclose(dml_ssm_fixture['se'], - dml_ssm_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_ssm_fixture["se"], dml_ssm_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) From 36ae8b24c559e945f954a53f2ce06a9562dfeae8 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:18:17 +0100 Subject: [PATCH 08/28] run ruff on plm --- doubleml/plm/__init__.py | 2 +- doubleml/plm/pliv.py | 14 ++++++-------- doubleml/plm/plr.py | 11 +++++------ doubleml/plm/tests/_utils_pliv_manual.py | 2 +- doubleml/plm/tests/_utils_pliv_partial_x_manual.py | 2 +- .../plm/tests/_utils_pliv_partial_xz_manual.py | 4 ++-- doubleml/plm/tests/_utils_pliv_partial_z_manual.py | 2 +- doubleml/plm/tests/_utils_plr_manual.py | 2 +- doubleml/plm/tests/conftest.py | 5 ++--- doubleml/plm/tests/test_pliv.py | 9 ++++----- .../plm/tests/test_pliv_external_predictions.py | 6 ++++-- doubleml/plm/tests/test_pliv_partial_x.py | 7 +++---- doubleml/plm/tests/test_pliv_partial_x_tune.py | 9 ++++----- doubleml/plm/tests/test_pliv_partial_xz.py | 7 +++---- doubleml/plm/tests/test_pliv_partial_xz_tune.py | 9 ++++----- doubleml/plm/tests/test_pliv_partial_z.py | 7 +++---- doubleml/plm/tests/test_pliv_partial_z_tune.py | 7 +++---- doubleml/plm/tests/test_pliv_tune.py | 10 +++++----- doubleml/plm/tests/test_plr.py | 11 +++++------ doubleml/plm/tests/test_plr_classifier.py | 10 +++++----- .../plm/tests/test_plr_external_predictions.py | 6 ++++-- doubleml/plm/tests/test_plr_multi_treat.py | 7 +++---- .../plm/tests/test_plr_reestimate_from_scores.py | 4 ++-- doubleml/plm/tests/test_plr_rep_cross.py | 10 +++++----- .../plm/tests/test_plr_set_ml_nuisance_pars.py | 4 ++-- .../plm/tests/test_plr_set_smpls_externally.py | 4 ++-- doubleml/plm/tests/test_plr_tune.py | 10 +++++----- 27 files changed, 86 insertions(+), 95 deletions(-) diff --git a/doubleml/plm/__init__.py b/doubleml/plm/__init__.py index 7cfe61f0d..e81f00c52 100644 --- a/doubleml/plm/__init__.py +++ b/doubleml/plm/__init__.py @@ -2,8 +2,8 @@ The :mod:`doubleml.plm` module implements double machine learning estimates based on partially linear models. """ -from .plr import DoubleMLPLR from .pliv import DoubleMLPLIV +from .plr import DoubleMLPLR __all__ = [ "DoubleMLPLR", diff --git a/doubleml/plm/pliv.py b/doubleml/plm/pliv.py index 100d4f69f..e9c037891 100644 --- a/doubleml/plm/pliv.py +++ b/doubleml/plm/pliv.py @@ -1,18 +1,16 @@ +import warnings + import numpy as np -from sklearn.utils import check_X_y -from sklearn.model_selection import KFold -from sklearn.model_selection import GridSearchCV, RandomizedSearchCV -from sklearn.linear_model import LinearRegression from sklearn.dummy import DummyRegressor - -import warnings +from sklearn.linear_model import LinearRegression +from sklearn.model_selection import GridSearchCV, KFold, RandomizedSearchCV +from sklearn.utils import check_X_y from ..double_ml import DoubleML from ..double_ml_data import DoubleMLData from ..double_ml_score_mixins import LinearScoreMixin - -from ..utils._estimation import _dml_cv_predict, _dml_tune from ..utils._checks import _check_finite_predictions +from ..utils._estimation import _dml_cv_predict, _dml_tune class DoubleMLPLIV(LinearScoreMixin, DoubleML): diff --git a/doubleml/plm/plr.py b/doubleml/plm/plr.py index 3f0a26eaf..c636db83c 100644 --- a/doubleml/plm/plr.py +++ b/doubleml/plm/plr.py @@ -1,17 +1,16 @@ +import warnings + import numpy as np import pandas as pd -from sklearn.utils import check_X_y from sklearn.base import clone - -import warnings +from sklearn.utils import check_X_y from ..double_ml import DoubleML from ..double_ml_data import DoubleMLData from ..double_ml_score_mixins import LinearScoreMixin -from ..utils.blp import DoubleMLBLP - +from ..utils._checks import _check_binary_predictions, _check_finite_predictions, _check_is_propensity, _check_score from ..utils._estimation import _dml_cv_predict, _dml_tune -from ..utils._checks import _check_score, _check_finite_predictions, _check_is_propensity, _check_binary_predictions +from ..utils.blp import DoubleMLBLP class DoubleMLPLR(LinearScoreMixin, DoubleML): diff --git a/doubleml/plm/tests/_utils_pliv_manual.py b/doubleml/plm/tests/_utils_pliv_manual.py index 32ff7cdb6..c9b6ef975 100644 --- a/doubleml/plm/tests/_utils_pliv_manual.py +++ b/doubleml/plm/tests/_utils_pliv_manual.py @@ -1,8 +1,8 @@ import numpy as np from sklearn.base import clone -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, tune_grid_search +from ...tests._utils_boot import boot_manual, draw_weights def fit_pliv(y, x, d, z, diff --git a/doubleml/plm/tests/_utils_pliv_partial_x_manual.py b/doubleml/plm/tests/_utils_pliv_partial_x_manual.py index 089b317ee..fe01a58bc 100644 --- a/doubleml/plm/tests/_utils_pliv_partial_x_manual.py +++ b/doubleml/plm/tests/_utils_pliv_partial_x_manual.py @@ -1,8 +1,8 @@ import numpy as np from sklearn.linear_model import LinearRegression -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, tune_grid_search +from ...tests._utils_boot import boot_manual, draw_weights def fit_pliv_partial_x(y, x, d, z, diff --git a/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py b/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py index b82c84df7..aa8cb2a53 100644 --- a/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py +++ b/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py @@ -1,8 +1,8 @@ import numpy as np -from sklearn.model_selection import KFold, GridSearchCV +from sklearn.model_selection import GridSearchCV, KFold -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, tune_grid_search +from ...tests._utils_boot import boot_manual, draw_weights def fit_pliv_partial_xz(y, x, d, z, diff --git a/doubleml/plm/tests/_utils_pliv_partial_z_manual.py b/doubleml/plm/tests/_utils_pliv_partial_z_manual.py index 631905d67..6f88fe991 100644 --- a/doubleml/plm/tests/_utils_pliv_partial_z_manual.py +++ b/doubleml/plm/tests/_utils_pliv_partial_z_manual.py @@ -1,7 +1,7 @@ import numpy as np -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, tune_grid_search +from ...tests._utils_boot import boot_manual, draw_weights def fit_pliv_partial_z(y, x, d, z, diff --git a/doubleml/plm/tests/_utils_plr_manual.py b/doubleml/plm/tests/_utils_plr_manual.py index 745de5219..dbc820601 100644 --- a/doubleml/plm/tests/_utils_plr_manual.py +++ b/doubleml/plm/tests/_utils_plr_manual.py @@ -2,8 +2,8 @@ import scipy from sklearn.base import clone, is_classifier -from ...tests._utils_boot import boot_manual, draw_weights from ...tests._utils import fit_predict, fit_predict_proba, tune_grid_search +from ...tests._utils_boot import boot_manual, draw_weights def fit_plr_multitreat(y, x, d, learner_l, learner_m, learner_g, all_smpls, score, diff --git a/doubleml/plm/tests/conftest.py b/doubleml/plm/tests/conftest.py index 925179e14..d61c8b4c4 100644 --- a/doubleml/plm/tests/conftest.py +++ b/doubleml/plm/tests/conftest.py @@ -1,11 +1,10 @@ import numpy as np import pandas as pd - import pytest from scipy.linalg import toeplitz - from sklearn.datasets import make_spd_matrix -from doubleml.datasets import make_plr_turrell2018, make_pliv_CHS2015 + +from doubleml.datasets import make_pliv_CHS2015, make_plr_turrell2018 def _g(x): diff --git a/doubleml/plm/tests/test_pliv.py b/doubleml/plm/tests/test_pliv.py index ce640aadb..ff2aa4938 100644 --- a/doubleml/plm/tests/test_pliv.py +++ b/doubleml/plm/tests/test_pliv.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import LinearRegression, Lasso from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import Lasso, LinearRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_manual import fit_pliv, boot_pliv +from ._utils_pliv_manual import boot_pliv, fit_pliv @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_external_predictions.py b/doubleml/plm/tests/test_pliv_external_predictions.py index 4b53f6f70..2755d2002 100644 --- a/doubleml/plm/tests/test_pliv_external_predictions.py +++ b/doubleml/plm/tests/test_pliv_external_predictions.py @@ -1,8 +1,10 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LinearRegression -from doubleml import DoubleMLPLIV, DoubleMLData + +from doubleml import DoubleMLData, DoubleMLPLIV from doubleml.datasets import make_pliv_CHS2015 from doubleml.utils import DMLDummyRegressor diff --git a/doubleml/plm/tests/test_pliv_partial_x.py b/doubleml/plm/tests/test_pliv_partial_x.py index d8593500b..338d42ef1 100644 --- a/doubleml/plm/tests/test_pliv_partial_x.py +++ b/doubleml/plm/tests/test_pliv_partial_x.py @@ -1,15 +1,14 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - from sklearn.linear_model import Lasso import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_partial_x_manual import fit_pliv_partial_x, boot_pliv_partial_x +from ._utils_pliv_partial_x_manual import boot_pliv_partial_x, fit_pliv_partial_x @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_partial_x_tune.py b/doubleml/plm/tests/test_pliv_partial_x_tune.py index bd91f60c0..fa9d6475b 100644 --- a/doubleml/plm/tests/test_pliv_partial_x_tune.py +++ b/doubleml/plm/tests/test_pliv_partial_x_tune.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import ElasticNet from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import ElasticNet import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_partial_x_manual import fit_pliv_partial_x, boot_pliv_partial_x, tune_nuisance_pliv_partial_x +from ._utils_pliv_partial_x_manual import boot_pliv_partial_x, fit_pliv_partial_x, tune_nuisance_pliv_partial_x @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_partial_xz.py b/doubleml/plm/tests/test_pliv_partial_xz.py index 4b14eeb05..cb9722c11 100644 --- a/doubleml/plm/tests/test_pliv_partial_xz.py +++ b/doubleml/plm/tests/test_pliv_partial_xz.py @@ -1,15 +1,14 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - from sklearn.linear_model import Lasso import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_partial_xz_manual import fit_pliv_partial_xz, boot_pliv_partial_xz +from ._utils_pliv_partial_xz_manual import boot_pliv_partial_xz, fit_pliv_partial_xz @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_partial_xz_tune.py b/doubleml/plm/tests/test_pliv_partial_xz_tune.py index 68c6d1bef..758cbde7b 100644 --- a/doubleml/plm/tests/test_pliv_partial_xz_tune.py +++ b/doubleml/plm/tests/test_pliv_partial_xz_tune.py @@ -1,16 +1,15 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - -from sklearn.linear_model import ElasticNet from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import ElasticNet import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_partial_xz_manual import fit_pliv_partial_xz, boot_pliv_partial_xz, tune_nuisance_pliv_partial_xz +from ._utils_pliv_partial_xz_manual import boot_pliv_partial_xz, fit_pliv_partial_xz, tune_nuisance_pliv_partial_xz @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_partial_z.py b/doubleml/plm/tests/test_pliv_partial_z.py index b4b2af639..3fb7a647b 100644 --- a/doubleml/plm/tests/test_pliv_partial_z.py +++ b/doubleml/plm/tests/test_pliv_partial_z.py @@ -1,15 +1,14 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - from sklearn.linear_model import Lasso import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_partial_z_manual import fit_pliv_partial_z, boot_pliv_partial_z +from ._utils_pliv_partial_z_manual import boot_pliv_partial_z, fit_pliv_partial_z @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_partial_z_tune.py b/doubleml/plm/tests/test_pliv_partial_z_tune.py index a52d1da6c..bdc322d19 100644 --- a/doubleml/plm/tests/test_pliv_partial_z_tune.py +++ b/doubleml/plm/tests/test_pliv_partial_z_tune.py @@ -1,15 +1,14 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone - from sklearn.linear_model import ElasticNet import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_pliv_partial_z_manual import fit_pliv_partial_z, boot_pliv_partial_z, tune_nuisance_pliv_partial_z +from ._utils_pliv_partial_z_manual import boot_pliv_partial_z, fit_pliv_partial_z, tune_nuisance_pliv_partial_z @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_pliv_tune.py b/doubleml/plm/tests/test_pliv_tune.py index bcd69d322..80240bd3b 100644 --- a/doubleml/plm/tests/test_pliv_tune.py +++ b/doubleml/plm/tests/test_pliv_tune.py @@ -1,13 +1,13 @@ -import numpy as np -import pytest import math -from sklearn.linear_model import Lasso, ElasticNet +import numpy as np +import pytest +from sklearn.linear_model import ElasticNet, Lasso import doubleml as dml -from ...tests._utils import draw_smpls, _clone -from ._utils_pliv_manual import fit_pliv, boot_pliv, tune_nuisance_pliv +from ...tests._utils import _clone, draw_smpls +from ._utils_pliv_manual import boot_pliv, fit_pliv, tune_nuisance_pliv @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_plr.py b/doubleml/plm/tests/test_plr.py index d46b914c4..ba1f132a2 100644 --- a/doubleml/plm/tests/test_plr.py +++ b/doubleml/plm/tests/test_plr.py @@ -1,18 +1,17 @@ -import pytest import math -import scipy + import numpy as np import pandas as pd - +import pytest +import scipy from sklearn.base import clone - -from sklearn.linear_model import LinearRegression, Lasso from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import Lasso, LinearRegression import doubleml as dml from ...tests._utils import draw_smpls -from ._utils_plr_manual import fit_plr, plr_dml2, boot_plr, fit_sensitivity_elements_plr +from ._utils_plr_manual import boot_plr, fit_plr, fit_sensitivity_elements_plr, plr_dml2 @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_plr_classifier.py b/doubleml/plm/tests/test_plr_classifier.py index 1fad89a93..e39bb46b1 100644 --- a/doubleml/plm/tests/test_plr_classifier.py +++ b/doubleml/plm/tests/test_plr_classifier.py @@ -1,15 +1,15 @@ -import numpy as np -import pytest import math -from sklearn.linear_model import Lasso, LogisticRegression +import numpy as np +import pytest from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import Lasso, LogisticRegression import doubleml as dml from doubleml.datasets import fetch_bonus -from ...tests._utils import draw_smpls, _clone -from ._utils_plr_manual import fit_plr, boot_plr +from ...tests._utils import _clone, draw_smpls +from ._utils_plr_manual import boot_plr, fit_plr bonus_data = fetch_bonus() diff --git a/doubleml/plm/tests/test_plr_external_predictions.py b/doubleml/plm/tests/test_plr_external_predictions.py index a06d34ab6..47644555d 100644 --- a/doubleml/plm/tests/test_plr_external_predictions.py +++ b/doubleml/plm/tests/test_plr_external_predictions.py @@ -1,8 +1,10 @@ +import math + import numpy as np import pytest -import math from sklearn.linear_model import LinearRegression -from doubleml import DoubleMLPLR, DoubleMLData + +from doubleml import DoubleMLData, DoubleMLPLR from doubleml.datasets import make_plr_CCDDHNR2018 from doubleml.utils import DMLDummyRegressor diff --git a/doubleml/plm/tests/test_plr_multi_treat.py b/doubleml/plm/tests/test_plr_multi_treat.py index 82efbb739..38e1713b5 100644 --- a/doubleml/plm/tests/test_plr_multi_treat.py +++ b/doubleml/plm/tests/test_plr_multi_treat.py @@ -1,13 +1,12 @@ import numpy as np import pytest - -from sklearn.linear_model import Lasso from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import Lasso import doubleml as dml -from ...tests._utils import draw_smpls, _clone -from ._utils_plr_manual import fit_plr_multitreat, boot_plr_multitreat, fit_sensitivity_elements_plr +from ...tests._utils import _clone, draw_smpls +from ._utils_plr_manual import boot_plr_multitreat, fit_plr_multitreat, fit_sensitivity_elements_plr @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_plr_reestimate_from_scores.py b/doubleml/plm/tests/test_plr_reestimate_from_scores.py index 2a555ec9b..83932f3bc 100644 --- a/doubleml/plm/tests/test_plr_reestimate_from_scores.py +++ b/doubleml/plm/tests/test_plr_reestimate_from_scores.py @@ -1,7 +1,7 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.linear_model import LinearRegression import doubleml as dml diff --git a/doubleml/plm/tests/test_plr_rep_cross.py b/doubleml/plm/tests/test_plr_rep_cross.py index 71b4dcea8..8b17a923a 100644 --- a/doubleml/plm/tests/test_plr_rep_cross.py +++ b/doubleml/plm/tests/test_plr_rep_cross.py @@ -1,14 +1,14 @@ -import numpy as np -import pytest import math -from sklearn.linear_model import LinearRegression +import numpy as np +import pytest from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LinearRegression import doubleml as dml -from ...tests._utils import draw_smpls, _clone -from ._utils_plr_manual import fit_plr, boot_plr +from ...tests._utils import _clone, draw_smpls +from ._utils_plr_manual import boot_plr, fit_plr @pytest.fixture(scope='module', diff --git a/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py b/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py index f86e2632b..dccd3fc3a 100644 --- a/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py +++ b/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py @@ -1,7 +1,7 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.linear_model import Lasso import doubleml as dml diff --git a/doubleml/plm/tests/test_plr_set_smpls_externally.py b/doubleml/plm/tests/test_plr_set_smpls_externally.py index eb5cb1e4b..9476f126c 100644 --- a/doubleml/plm/tests/test_plr_set_smpls_externally.py +++ b/doubleml/plm/tests/test_plr_set_smpls_externally.py @@ -1,7 +1,7 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.linear_model import LinearRegression import doubleml as dml diff --git a/doubleml/plm/tests/test_plr_tune.py b/doubleml/plm/tests/test_plr_tune.py index c0ba637de..6607f33eb 100644 --- a/doubleml/plm/tests/test_plr_tune.py +++ b/doubleml/plm/tests/test_plr_tune.py @@ -1,13 +1,13 @@ -import numpy as np -import pytest import math -from sklearn.linear_model import Lasso, ElasticNet +import numpy as np +import pytest +from sklearn.linear_model import ElasticNet, Lasso import doubleml as dml -from ...tests._utils import draw_smpls, _clone -from ._utils_plr_manual import fit_plr, boot_plr, tune_nuisance_plr +from ...tests._utils import _clone, draw_smpls +from ._utils_plr_manual import boot_plr, fit_plr, tune_nuisance_plr @pytest.fixture(scope='module', From 1c31e8b76da3fc5a2048950334269e23fdbe97d5 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:22:21 +0100 Subject: [PATCH 09/28] reformat plm --- doubleml/plm/pliv.py | 783 +++++++++--------- doubleml/plm/plr.py | 330 ++++---- doubleml/plm/tests/_utils_pliv_manual.py | 177 ++-- .../plm/tests/_utils_pliv_partial_x_manual.py | 83 +- .../tests/_utils_pliv_partial_xz_manual.py | 84 +- .../plm/tests/_utils_pliv_partial_z_manual.py | 43 +- doubleml/plm/tests/_utils_plr_manual.py | 235 ++++-- doubleml/plm/tests/conftest.py | 138 +-- doubleml/plm/tests/test_pliv.py | 96 +-- .../tests/test_pliv_external_predictions.py | 8 +- doubleml/plm/tests/test_pliv_partial_x.py | 73 +- .../plm/tests/test_pliv_partial_x_tune.py | 153 ++-- doubleml/plm/tests/test_pliv_partial_xz.py | 75 +- .../plm/tests/test_pliv_partial_xz_tune.py | 155 ++-- doubleml/plm/tests/test_pliv_partial_z.py | 71 +- .../plm/tests/test_pliv_partial_z_tune.py | 89 +- doubleml/plm/tests/test_pliv_tune.py | 176 ++-- doubleml/plm/tests/test_plr.py | 276 +++--- doubleml/plm/tests/test_plr_classifier.py | 75 +- doubleml/plm/tests/test_plr_multi_treat.py | 124 ++- .../tests/test_plr_reestimate_from_scores.py | 45 +- doubleml/plm/tests/test_plr_rep_cross.py | 125 +-- .../tests/test_plr_set_ml_nuisance_pars.py | 64 +- .../tests/test_plr_set_smpls_externally.py | 44 +- doubleml/plm/tests/test_plr_tune.py | 145 ++-- 25 files changed, 2001 insertions(+), 1666 deletions(-) diff --git a/doubleml/plm/pliv.py b/doubleml/plm/pliv.py index e9c037891..95e590339 100644 --- a/doubleml/plm/pliv.py +++ b/doubleml/plm/pliv.py @@ -93,169 +93,127 @@ class DoubleMLPLIV(LinearScoreMixin, DoubleML): :math:`V` are stochastic errors. """ - def __init__(self, - obj_dml_data, - ml_l, - ml_m, - ml_r, - ml_g=None, - n_folds=5, - n_rep=1, - score='partialling out', - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + def __init__( + self, + obj_dml_data, + ml_l, + ml_m, + ml_r, + ml_g=None, + n_folds=5, + n_rep=1, + score="partialling out", + draw_sample_splitting=True, + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) self.partialX = True self.partialZ = False self._check_score(self.score) - _ = self._check_learner(ml_l, 'ml_l', regressor=True, classifier=False) - _ = self._check_learner(ml_m, 'ml_m', regressor=True, classifier=False) - _ = self._check_learner(ml_r, 'ml_r', regressor=True, classifier=False) - self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_r': ml_r} + _ = self._check_learner(ml_l, "ml_l", regressor=True, classifier=False) + _ = self._check_learner(ml_m, "ml_m", regressor=True, classifier=False) + _ = self._check_learner(ml_r, "ml_r", regressor=True, classifier=False) + self._learner = {"ml_l": ml_l, "ml_m": ml_m, "ml_r": ml_r} if ml_g is not None: - if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner['ml_g'] = ml_g + if (isinstance(self.score, str) & (self.score == "IV-type")) | callable(self.score): + _ = self._check_learner(ml_g, "ml_g", regressor=True, classifier=False) + self._learner["ml_g"] = ml_g else: - assert (isinstance(self.score, str) & (self.score == 'partialling out')) - warnings.warn(('A learner ml_g has been provided for score = "partialling out" but will be ignored. "' - 'A learner ml_g is not required for estimation.')) - elif isinstance(self.score, str) & (self.score == 'IV-type'): + assert isinstance(self.score, str) & (self.score == "partialling out") + warnings.warn( + ( + 'A learner ml_g has been provided for score = "partialling out" but will be ignored. "' + "A learner ml_g is not required for estimation." + ) + ) + elif isinstance(self.score, str) & (self.score == "IV-type"): raise ValueError("For score = 'IV-type', learners ml_l, ml_m, ml_r and ml_g need to be specified.") - self._predict_method = {'ml_l': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} - if 'ml_g' in self._learner: - self._predict_method['ml_g'] = 'predict' + self._predict_method = {"ml_l": "predict", "ml_m": "predict", "ml_r": "predict"} + if "ml_g" in self._learner: + self._predict_method["ml_g"] = "predict" self._initialize_ml_nuisance_params() self._external_predictions_implemented = True @classmethod - def _partialX(cls, - obj_dml_data, - ml_l, - ml_m, - ml_r, - ml_g=None, - n_folds=5, - n_rep=1, - score='partialling out', - draw_sample_splitting=True): - obj = cls(obj_dml_data, - ml_l, - ml_m, - ml_r, - ml_g, - n_folds, - n_rep, - score, - draw_sample_splitting) + def _partialX( + cls, obj_dml_data, ml_l, ml_m, ml_r, ml_g=None, n_folds=5, n_rep=1, score="partialling out", draw_sample_splitting=True + ): + obj = cls(obj_dml_data, ml_l, ml_m, ml_r, ml_g, n_folds, n_rep, score, draw_sample_splitting) obj._check_data(obj._dml_data) obj.partialX = True obj.partialZ = False obj._check_score(obj.score) - _ = obj._check_learner(ml_l, 'ml_l', regressor=True, classifier=False) - _ = obj._check_learner(ml_m, 'ml_m', regressor=True, classifier=False) - _ = obj._check_learner(ml_r, 'ml_r', regressor=True, classifier=False) - obj._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_r': ml_r} - obj._predict_method = {'ml_l': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} + _ = obj._check_learner(ml_l, "ml_l", regressor=True, classifier=False) + _ = obj._check_learner(ml_m, "ml_m", regressor=True, classifier=False) + _ = obj._check_learner(ml_r, "ml_r", regressor=True, classifier=False) + obj._learner = {"ml_l": ml_l, "ml_m": ml_m, "ml_r": ml_r} + obj._predict_method = {"ml_l": "predict", "ml_m": "predict", "ml_r": "predict"} obj._initialize_ml_nuisance_params() return obj @classmethod - def _partialZ(cls, - obj_dml_data, - ml_r, - n_folds=5, - n_rep=1, - score='partialling out', - draw_sample_splitting=True): + def _partialZ(cls, obj_dml_data, ml_r, n_folds=5, n_rep=1, score="partialling out", draw_sample_splitting=True): # to pass the checks for the learners, we temporarily set ml_l and ml_m to DummyRegressor() - obj = cls(obj_dml_data, - DummyRegressor(), - DummyRegressor(), - ml_r, - None, - n_folds, - n_rep, - score, - draw_sample_splitting) + obj = cls(obj_dml_data, DummyRegressor(), DummyRegressor(), ml_r, None, n_folds, n_rep, score, draw_sample_splitting) obj._check_data(obj._dml_data) obj.partialX = False obj.partialZ = True obj._check_score(obj.score) - _ = obj._check_learner(ml_r, 'ml_r', regressor=True, classifier=False) - obj._learner = {'ml_r': ml_r} - obj._predict_method = {'ml_r': 'predict'} + _ = obj._check_learner(ml_r, "ml_r", regressor=True, classifier=False) + obj._learner = {"ml_r": ml_r} + obj._predict_method = {"ml_r": "predict"} obj._initialize_ml_nuisance_params() return obj @classmethod - def _partialXZ(cls, - obj_dml_data, - ml_l, - ml_m, - ml_r, - n_folds=5, - n_rep=1, - score='partialling out', - draw_sample_splitting=True): - obj = cls(obj_dml_data, - ml_l, - ml_m, - ml_r, - None, - n_folds, - n_rep, - score, - draw_sample_splitting) + def _partialXZ( + cls, obj_dml_data, ml_l, ml_m, ml_r, n_folds=5, n_rep=1, score="partialling out", draw_sample_splitting=True + ): + obj = cls(obj_dml_data, ml_l, ml_m, ml_r, None, n_folds, n_rep, score, draw_sample_splitting) obj._check_data(obj._dml_data) obj.partialX = True obj.partialZ = True obj._check_score(obj.score) - _ = obj._check_learner(ml_l, 'ml_l', regressor=True, classifier=False) - _ = obj._check_learner(ml_m, 'ml_m', regressor=True, classifier=False) - _ = obj._check_learner(ml_r, 'ml_r', regressor=True, classifier=False) - obj._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_r': ml_r} - obj._predict_method = {'ml_l': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} + _ = obj._check_learner(ml_l, "ml_l", regressor=True, classifier=False) + _ = obj._check_learner(ml_m, "ml_m", regressor=True, classifier=False) + _ = obj._check_learner(ml_r, "ml_r", regressor=True, classifier=False) + obj._learner = {"ml_l": ml_l, "ml_m": ml_m, "ml_r": ml_r} + obj._predict_method = {"ml_l": "predict", "ml_m": "predict", "ml_r": "predict"} obj._initialize_ml_nuisance_params() return obj def _initialize_ml_nuisance_params(self): if self.partialX & (not self.partialZ) & (self._dml_data.n_instr > 1): - param_names = ['ml_l', 'ml_r'] + ['ml_m_' + z_col for z_col in self._dml_data.z_cols] + param_names = ["ml_l", "ml_r"] + ["ml_m_" + z_col for z_col in self._dml_data.z_cols] else: param_names = self._learner.keys() - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in param_names} + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in param_names} def _check_score(self, score): if isinstance(score, str): if self.partialX & (not self.partialZ) & (self._dml_data.n_instr == 1): - valid_score = ['partialling out', 'IV-type'] + valid_score = ["partialling out", "IV-type"] else: - valid_score = ['partialling out'] + valid_score = ["partialling out"] if score not in valid_score: - raise ValueError('Invalid score ' + score + '. ' + - 'Valid score ' + ' or '.join(valid_score) + '.') + raise ValueError("Invalid score " + score + ". " + "Valid score " + " or ".join(valid_score) + ".") else: if not callable(score): - raise TypeError('score should be either a string or a callable. ' - '%r was passed.' % score) + raise TypeError("score should be either a string or a callable. " "%r was passed." % score) return score def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) if obj_dml_data.n_instr == 0: - raise ValueError('Incompatible data. ' + - 'At least one variable must be set as instrumental variable. ' - 'To fit a partially linear regression model without instrumental variable(s) ' - 'use DoubleMLPLR instead of DoubleMLPLIV.') + raise ValueError( + "Incompatible data. " + "At least one variable must be set as instrumental variable. " + "To fit a partially linear regression model without instrumental variable(s) " + "use DoubleMLPLR instead of DoubleMLPLIV." + ) return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): @@ -264,134 +222,158 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa elif (not self.partialX) & self.partialZ: psi_elements, preds = self._nuisance_est_partial_z(smpls, n_jobs_cv, return_models) else: - assert (self.partialX & self.partialZ) + assert self.partialX & self.partialZ psi_elements, preds = self._nuisance_est_partial_xz(smpls, n_jobs_cv, return_models) return psi_elements, preds - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): if self.partialX & (not self.partialZ): - res = self._nuisance_tuning_partial_x(smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search) + res = self._nuisance_tuning_partial_x( + smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ) elif (not self.partialX) & self.partialZ: - res = self._nuisance_tuning_partial_z(smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search) + res = self._nuisance_tuning_partial_z( + smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ) else: - assert (self.partialX & self.partialZ) - res = self._nuisance_tuning_partial_xz(smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search) + assert self.partialX & self.partialZ + res = self._nuisance_tuning_partial_xz( + smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ) return res def _nuisance_est_partial_x(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # nuisance l - if external_predictions['ml_l'] is not None: - l_hat = {'preds': external_predictions['ml_l'], - 'targets': None, - 'models': None} + if external_predictions["ml_l"] is not None: + l_hat = {"preds": external_predictions["ml_l"], "targets": None, "models": None} else: - l_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_l'), method=self._predict_method['ml_l'], - return_models=return_models) - _check_finite_predictions(l_hat['preds'], self._learner['ml_l'], 'ml_l', smpls) - - predictions = {'ml_l': l_hat['preds']} - targets = {'ml_l': l_hat['targets']} - models = {'ml_l': l_hat['models']} + l_hat = _dml_cv_predict( + self._learner["ml_l"], + x, + y, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_l"), + method=self._predict_method["ml_l"], + return_models=return_models, + ) + _check_finite_predictions(l_hat["preds"], self._learner["ml_l"], "ml_l", smpls) + + predictions = {"ml_l": l_hat["preds"]} + targets = {"ml_l": l_hat["targets"]} + models = {"ml_l": l_hat["models"]} # nuisance m if self._dml_data.n_instr == 1: # one instrument: just identified - x, z = check_X_y(x, np.ravel(self._dml_data.z), - force_all_finite=False) - if external_predictions['ml_m'] is not None: - m_hat = {'preds': external_predictions['ml_m'], - 'targets': None, - 'models': None} + x, z = check_X_y(x, np.ravel(self._dml_data.z), force_all_finite=False) + if external_predictions["ml_m"] is not None: + m_hat = {"preds": external_predictions["ml_m"], "targets": None, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, z, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - predictions['ml_m'] = m_hat['preds'] - targets['ml_m'] = m_hat['targets'] - models['ml_m'] = m_hat['models'] + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + z, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + predictions["ml_m"] = m_hat["preds"] + targets["ml_m"] = m_hat["targets"] + models["ml_m"] = m_hat["models"] else: # several instruments: 2SLS - m_hat = {'preds': np.full((self._dml_data.n_obs, self._dml_data.n_instr), np.nan), - 'targets': [None] * self._dml_data.n_instr, - 'models': [None] * self._dml_data.n_instr} + m_hat = { + "preds": np.full((self._dml_data.n_obs, self._dml_data.n_instr), np.nan), + "targets": [None] * self._dml_data.n_instr, + "models": [None] * self._dml_data.n_instr, + } for i_instr in range(self._dml_data.n_instr): z = self._dml_data.z - x, this_z = check_X_y(x, z[:, i_instr], - force_all_finite=False) - if external_predictions['ml_m_' + self._dml_data.z_cols[i_instr]] is not None: - m_hat['preds'][:, i_instr] = external_predictions['ml_m_' + self._dml_data.z_cols[i_instr]] - predictions['ml_m_' + self._dml_data.z_cols[i_instr]] = external_predictions[ - 'ml_m_' + self._dml_data.z_cols[i_instr]] - targets['ml_m_' + self._dml_data.z_cols[i_instr]] = None - models['ml_m_' + self._dml_data.z_cols[i_instr]] = None + x, this_z = check_X_y(x, z[:, i_instr], force_all_finite=False) + if external_predictions["ml_m_" + self._dml_data.z_cols[i_instr]] is not None: + m_hat["preds"][:, i_instr] = external_predictions["ml_m_" + self._dml_data.z_cols[i_instr]] + predictions["ml_m_" + self._dml_data.z_cols[i_instr]] = external_predictions[ + "ml_m_" + self._dml_data.z_cols[i_instr] + ] + targets["ml_m_" + self._dml_data.z_cols[i_instr]] = None + models["ml_m_" + self._dml_data.z_cols[i_instr]] = None else: - res_cv_predict = _dml_cv_predict(self._learner['ml_m'], x, this_z, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m_' + self._dml_data.z_cols[i_instr]), - method=self._predict_method['ml_m'], return_models=return_models) + res_cv_predict = _dml_cv_predict( + self._learner["ml_m"], + x, + this_z, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m_" + self._dml_data.z_cols[i_instr]), + method=self._predict_method["ml_m"], + return_models=return_models, + ) - m_hat['preds'][:, i_instr] = res_cv_predict['preds'] + m_hat["preds"][:, i_instr] = res_cv_predict["preds"] - predictions['ml_m_' + self._dml_data.z_cols[i_instr]] = res_cv_predict['preds'] - targets['ml_m_' + self._dml_data.z_cols[i_instr]] = res_cv_predict['targets'] - models['ml_m_' + self._dml_data.z_cols[i_instr]] = res_cv_predict['models'] + predictions["ml_m_" + self._dml_data.z_cols[i_instr]] = res_cv_predict["preds"] + targets["ml_m_" + self._dml_data.z_cols[i_instr]] = res_cv_predict["targets"] + models["ml_m_" + self._dml_data.z_cols[i_instr]] = res_cv_predict["models"] - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) # nuisance r - if external_predictions['ml_r'] is not None: - r_hat = {'preds': external_predictions['ml_r'], - 'targets': None, - 'models': None} + if external_predictions["ml_r"] is not None: + r_hat = {"preds": external_predictions["ml_r"], "targets": None, "models": None} else: - r_hat = _dml_cv_predict(self._learner['ml_r'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_r'), method=self._predict_method['ml_r'], - return_models=return_models) - _check_finite_predictions(r_hat['preds'], self._learner['ml_r'], 'ml_r', smpls) - predictions['ml_r'] = r_hat['preds'] - targets['ml_r'] = r_hat['targets'] - models['ml_r'] = r_hat['models'] - - g_hat = {'preds': None, 'targets': None, 'models': None} - if (self._dml_data.n_instr == 1) & ('ml_g' in self._learner): + r_hat = _dml_cv_predict( + self._learner["ml_r"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_r"), + method=self._predict_method["ml_r"], + return_models=return_models, + ) + _check_finite_predictions(r_hat["preds"], self._learner["ml_r"], "ml_r", smpls) + predictions["ml_r"] = r_hat["preds"] + targets["ml_r"] = r_hat["targets"] + models["ml_r"] = r_hat["models"] + + g_hat = {"preds": None, "targets": None, "models": None} + if (self._dml_data.n_instr == 1) & ("ml_g" in self._learner): # an estimate of g is obtained for the IV-type score and callable scores # get an initial estimate for theta using the partialling out score - if external_predictions['ml_g'] is not None: - g_hat = {'preds': external_predictions['ml_g'], - 'targets': None, - 'models': None} + if external_predictions["ml_g"] is not None: + g_hat = {"preds": external_predictions["ml_g"], "targets": None, "models": None} else: - psi_a = -np.multiply(d - r_hat['preds'], z - m_hat['preds']) - psi_b = np.multiply(z - m_hat['preds'], y - l_hat['preds']) + psi_a = -np.multiply(d - r_hat["preds"], z - m_hat["preds"]) + psi_b = np.multiply(z - m_hat["preds"], y - l_hat["preds"]) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_g'], x, y - theta_initial * d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat['preds'], self._learner['ml_g'], 'ml_g', smpls) - - predictions['ml_g'] = g_hat['preds'] - targets['ml_g'] = g_hat['targets'] - models['ml_g'] = g_hat['models'] - psi_a, psi_b = self._score_elements(y, z, d, - l_hat['preds'], m_hat['preds'], r_hat['preds'], g_hat['preds'], - smpls) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - predictions = {'predictions': predictions, - 'targets': targets, - 'models': models - } + g_hat = _dml_cv_predict( + self._learner["ml_g"], + x, + y - theta_initial * d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat["preds"], self._learner["ml_g"], "ml_g", smpls) + + predictions["ml_g"] = g_hat["preds"] + targets["ml_g"] = g_hat["targets"] + models["ml_g"] = g_hat["models"] + psi_a, psi_b = self._score_elements(y, z, d, l_hat["preds"], m_hat["preds"], r_hat["preds"], g_hat["preds"], smpls) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + predictions = {"predictions": predictions, "targets": targets, "models": models} return psi_elements, predictions @@ -409,168 +391,210 @@ def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, g_hat, smpls): if isinstance(self.score, str): if self._dml_data.n_instr == 1: - if self.score == 'partialling out': + if self.score == "partialling out": psi_a = -np.multiply(w_hat, v_hat) psi_b = np.multiply(v_hat, u_hat) else: - assert self.score == 'IV-type' + assert self.score == "IV-type" psi_a = -np.multiply(v_hat, d) psi_b = np.multiply(v_hat, y - g_hat) else: - assert self.score == 'partialling out' + assert self.score == "partialling out" psi_a = -np.multiply(w_hat, r_hat_tilde) psi_b = np.multiply(r_hat_tilde, u_hat) else: assert callable(self.score) if self._dml_data.n_instr > 1: - raise NotImplementedError('Callable score not implemented for DoubleMLPLIV.partialX ' - 'with several instruments.') + raise NotImplementedError( + "Callable score not implemented for DoubleMLPLIV.partialX " "with several instruments." + ) else: assert self._dml_data.n_instr == 1 - psi_a, psi_b = self.score(y=y, z=z, d=d, - l_hat=l_hat, m_hat=m_hat, r_hat=r_hat, g_hat=g_hat, - smpls=smpls) + psi_a, psi_b = self.score(y=y, z=z, d=d, l_hat=l_hat, m_hat=m_hat, r_hat=r_hat, g_hat=g_hat, smpls=smpls) return psi_a, psi_b def _nuisance_est_partial_z(self, smpls, n_jobs_cv, return_models=False): y = self._dml_data.y - xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), - self._dml_data.d, - force_all_finite=False) + xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), self._dml_data.d, force_all_finite=False) # nuisance m - r_hat = _dml_cv_predict(self._learner['ml_r'], xz, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_r'), method=self._predict_method['ml_r'], - return_models=return_models) - _check_finite_predictions(r_hat['preds'], self._learner['ml_r'], 'ml_r', smpls) + r_hat = _dml_cv_predict( + self._learner["ml_r"], + xz, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_r"), + method=self._predict_method["ml_r"], + return_models=return_models, + ) + _check_finite_predictions(r_hat["preds"], self._learner["ml_r"], "ml_r", smpls) if isinstance(self.score, str): - assert self.score == 'partialling out' - psi_a = -np.multiply(r_hat['preds'], d) - psi_b = np.multiply(r_hat['preds'], y) + assert self.score == "partialling out" + psi_a = -np.multiply(r_hat["preds"], d) + psi_b = np.multiply(r_hat["preds"], y) else: assert callable(self.score) - raise NotImplementedError('Callable score not implemented for DoubleMLPLIV.partialZ.') + raise NotImplementedError("Callable score not implemented for DoubleMLPLIV.partialZ.") - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_r': r_hat['preds']}, - 'targets': {'ml_r': r_hat['targets']}, - 'models': {'ml_r': r_hat['models']}} + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_r": r_hat["preds"]}, + "targets": {"ml_r": r_hat["targets"]}, + "models": {"ml_r": r_hat["models"]}, + } return psi_elements, preds def _nuisance_est_partial_xz(self, smpls, n_jobs_cv, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), - self._dml_data.d, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), self._dml_data.d, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # nuisance l - l_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_l'), method=self._predict_method['ml_l'], - return_models=return_models) - _check_finite_predictions(l_hat['preds'], self._learner['ml_l'], 'ml_l', smpls) + l_hat = _dml_cv_predict( + self._learner["ml_l"], + x, + y, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_l"), + method=self._predict_method["ml_l"], + return_models=return_models, + ) + _check_finite_predictions(l_hat["preds"], self._learner["ml_l"], "ml_l", smpls) # nuisance m - m_hat = _dml_cv_predict(self._learner['ml_m'], xz, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), return_train_preds=True, - method=self._predict_method['ml_m'], return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + xz, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + return_train_preds=True, + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) # nuisance r - m_hat_tilde = _dml_cv_predict(self._learner['ml_r'], x, m_hat['train_preds'], smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_r'), method=self._predict_method['ml_r'], - return_models=return_models) - _check_finite_predictions(m_hat_tilde['preds'], self._learner['ml_r'], 'ml_r', smpls) + m_hat_tilde = _dml_cv_predict( + self._learner["ml_r"], + x, + m_hat["train_preds"], + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_r"), + method=self._predict_method["ml_r"], + return_models=return_models, + ) + _check_finite_predictions(m_hat_tilde["preds"], self._learner["ml_r"], "ml_r", smpls) # compute residuals - u_hat = y - l_hat['preds'] - w_hat = d - m_hat_tilde['preds'] + u_hat = y - l_hat["preds"] + w_hat = d - m_hat_tilde["preds"] if isinstance(self.score, str): - assert self.score == 'partialling out' - psi_a = -np.multiply(w_hat, (m_hat['preds']-m_hat_tilde['preds'])) - psi_b = np.multiply((m_hat['preds']-m_hat_tilde['preds']), u_hat) + assert self.score == "partialling out" + psi_a = -np.multiply(w_hat, (m_hat["preds"] - m_hat_tilde["preds"])) + psi_b = np.multiply((m_hat["preds"] - m_hat_tilde["preds"]), u_hat) else: assert callable(self.score) - raise NotImplementedError('Callable score not implemented for DoubleMLPLIV.partialXZ.') - - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_l': l_hat['preds'], - 'ml_m': m_hat['preds'], - 'ml_r': m_hat_tilde['preds']}, - 'targets': {'ml_l': l_hat['targets'], - 'ml_m': m_hat['targets'], - 'ml_r': m_hat_tilde['targets']}, - 'models': {'ml_l': l_hat['models'], - 'ml_m': m_hat['models'], - 'ml_r': m_hat_tilde['models']} - } + raise NotImplementedError("Callable score not implemented for DoubleMLPLIV.partialXZ.") + + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_l": l_hat["preds"], "ml_m": m_hat["preds"], "ml_r": m_hat_tilde["preds"]}, + "targets": {"ml_l": l_hat["targets"], "ml_m": m_hat["targets"], "ml_r": m_hat_tilde["targets"]}, + "models": {"ml_l": l_hat["models"], "ml_m": m_hat["models"], "ml_r": m_hat_tilde["models"]}, + } return psi_elements, preds - def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning_partial_x( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_l': None, - 'ml_m': None, - 'ml_r': None, - 'ml_g': None} + scoring_methods = {"ml_l": None, "ml_m": None, "ml_r": None, "ml_g": None} train_inds = [train_index for (train_index, _) in smpls] - l_tune_res = _dml_tune(y, x, train_inds, - self._learner['ml_l'], param_grids['ml_l'], scoring_methods['ml_l'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + l_tune_res = _dml_tune( + y, + x, + train_inds, + self._learner["ml_l"], + param_grids["ml_l"], + scoring_methods["ml_l"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) if self._dml_data.n_instr > 1: # several instruments: 2SLS m_tune_res = {instr_var: list() for instr_var in self._dml_data.z_cols} z = self._dml_data.z for i_instr in range(self._dml_data.n_instr): - x, this_z = check_X_y(x, z[:, i_instr], - force_all_finite=False) - m_tune_res[self._dml_data.z_cols[i_instr]] = _dml_tune(this_z, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], - scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, - n_iter_randomized_search) + x, this_z = check_X_y(x, z[:, i_instr], force_all_finite=False) + m_tune_res[self._dml_data.z_cols[i_instr]] = _dml_tune( + this_z, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) else: # one instrument: just identified - x, z = check_X_y(x, np.ravel(self._dml_data.z), - force_all_finite=False) - m_tune_res = _dml_tune(z, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - - r_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_r'], param_grids['ml_r'], scoring_methods['ml_r'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + x, z = check_X_y(x, np.ravel(self._dml_data.z), force_all_finite=False) + m_tune_res = _dml_tune( + z, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + + r_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_r"], + param_grids["ml_r"], + scoring_methods["ml_r"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) l_best_params = [xx.best_params_ for xx in l_tune_res] r_best_params = [xx.best_params_ for xx in r_tune_res] if self._dml_data.n_instr > 1: - params = {'ml_l': l_best_params, - 'ml_r': r_best_params} + params = {"ml_l": l_best_params, "ml_r": r_best_params} for instr_var in self._dml_data.z_cols: - params['ml_m_' + instr_var] = [xx.best_params_ for xx in m_tune_res[instr_var]] - tune_res = {'l_tune': l_tune_res, - 'm_tune': m_tune_res, - 'r_tune': r_tune_res} + params["ml_m_" + instr_var] = [xx.best_params_ for xx in m_tune_res[instr_var]] + tune_res = {"l_tune": l_tune_res, "m_tune": m_tune_res, "r_tune": r_tune_res} else: m_best_params = [xx.best_params_ for xx in m_tune_res] # an ML model for g is obtained for the IV-type score and callable scores - if 'ml_g' in self._learner: + if "ml_g" in self._learner: # construct an initial theta estimate from the tuned models using the partialling out score l_hat = np.full_like(y, np.nan) m_hat = np.full_like(z, np.nan) @@ -582,110 +606,131 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold psi_a = -np.multiply(d - r_hat, z - m_hat) psi_b = np.multiply(z - m_hat, y - l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) - g_tune_res = _dml_tune(y - theta_initial * d, x, train_inds, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g_tune_res = _dml_tune( + y - theta_initial * d, + x, + train_inds, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g_best_params = [xx.best_params_ for xx in g_tune_res] - params = {'ml_l': l_best_params, - 'ml_m': m_best_params, - 'ml_r': r_best_params, - 'ml_g': g_best_params} - tune_res = {'l_tune': l_tune_res, - 'm_tune': m_tune_res, - 'r_tune': r_tune_res, - 'g_tune': g_tune_res} + params = {"ml_l": l_best_params, "ml_m": m_best_params, "ml_r": r_best_params, "ml_g": g_best_params} + tune_res = {"l_tune": l_tune_res, "m_tune": m_tune_res, "r_tune": r_tune_res, "g_tune": g_tune_res} else: - params = {'ml_l': l_best_params, - 'ml_m': m_best_params, - 'ml_r': r_best_params} - tune_res = {'l_tune': l_tune_res, - 'm_tune': m_tune_res, - 'r_tune': r_tune_res} + params = {"ml_l": l_best_params, "ml_m": m_best_params, "ml_r": r_best_params} + tune_res = {"l_tune": l_tune_res, "m_tune": m_tune_res, "r_tune": r_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res - def _nuisance_tuning_partial_z(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), - self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning_partial_z( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), self._dml_data.d, force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_r': None} + scoring_methods = {"ml_r": None} train_inds = [train_index for (train_index, _) in smpls] - m_tune_res = _dml_tune(d, xz, train_inds, - self._learner['ml_r'], param_grids['ml_r'], scoring_methods['ml_r'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + m_tune_res = _dml_tune( + d, + xz, + train_inds, + self._learner["ml_r"], + param_grids["ml_r"], + scoring_methods["ml_r"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_r': m_best_params} + params = {"ml_r": m_best_params} - tune_res = {'r_tune': m_tune_res} + tune_res = {"r_tune": m_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res - def _nuisance_tuning_partial_xz(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), - self._dml_data.d, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning_partial_xz( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + xz, d = check_X_y(np.hstack((self._dml_data.x, self._dml_data.z)), self._dml_data.d, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_l': None, - 'ml_m': None, - 'ml_r': None} + scoring_methods = {"ml_l": None, "ml_m": None, "ml_r": None} train_inds = [train_index for (train_index, _) in smpls] - l_tune_res = _dml_tune(y, x, train_inds, - self._learner['ml_l'], param_grids['ml_l'], scoring_methods['ml_l'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - m_tune_res = _dml_tune(d, xz, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + l_tune_res = _dml_tune( + y, + x, + train_inds, + self._learner["ml_l"], + param_grids["ml_l"], + scoring_methods["ml_l"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + m_tune_res = _dml_tune( + d, + xz, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) r_tune_res = list() for idx, (train_index, _) in enumerate(smpls): m_hat = m_tune_res[idx].predict(xz[train_index, :]) r_tune_resampling = KFold(n_splits=n_folds_tune, shuffle=True) - if search_mode == 'grid_search': - r_grid_search = GridSearchCV(self._learner['ml_r'], param_grids['ml_r'], - scoring=scoring_methods['ml_r'], - cv=r_tune_resampling, n_jobs=n_jobs_cv) + if search_mode == "grid_search": + r_grid_search = GridSearchCV( + self._learner["ml_r"], + param_grids["ml_r"], + scoring=scoring_methods["ml_r"], + cv=r_tune_resampling, + n_jobs=n_jobs_cv, + ) else: - assert search_mode == 'randomized_search' - r_grid_search = RandomizedSearchCV(self._learner['ml_r'], param_grids['ml_r'], - scoring=scoring_methods['ml_r'], - cv=r_tune_resampling, n_jobs=n_jobs_cv, - n_iter=n_iter_randomized_search) + assert search_mode == "randomized_search" + r_grid_search = RandomizedSearchCV( + self._learner["ml_r"], + param_grids["ml_r"], + scoring=scoring_methods["ml_r"], + cv=r_tune_resampling, + n_jobs=n_jobs_cv, + n_iter=n_iter_randomized_search, + ) r_tune_res.append(r_grid_search.fit(x[train_index, :], m_hat)) l_best_params = [xx.best_params_ for xx in l_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] r_best_params = [xx.best_params_ for xx in r_tune_res] - params = {'ml_l': l_best_params, - 'ml_m': m_best_params, - 'ml_r': r_best_params} + params = {"ml_l": l_best_params, "ml_m": m_best_params, "ml_r": r_best_params} - tune_res = {'l_tune': l_tune_res, - 'm_tune': m_tune_res, - 'r_tune': r_tune_res} + tune_res = {"l_tune": l_tune_res, "m_tune": m_tune_res, "r_tune": r_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res diff --git a/doubleml/plm/plr.py b/doubleml/plm/plr.py index c636db83c..23c65b06b 100644 --- a/doubleml/plm/plr.py +++ b/doubleml/plm/plr.py @@ -88,146 +88,147 @@ class DoubleMLPLR(LinearScoreMixin, DoubleML): and :math:`\\zeta` and :math:`V` are stochastic errors. """ - def __init__(self, - obj_dml_data, - ml_l, - ml_m, - ml_g=None, - n_folds=5, - n_rep=1, - score='partialling out', - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + def __init__( + self, obj_dml_data, ml_l, ml_m, ml_g=None, n_folds=5, n_rep=1, score="partialling out", draw_sample_splitting=True + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) - valid_scores = ['IV-type', 'partialling out'] + valid_scores = ["IV-type", "partialling out"] _check_score(self.score, valid_scores, allow_callable=True) - _ = self._check_learner(ml_l, 'ml_l', regressor=True, classifier=False) - ml_m_is_classifier = self._check_learner(ml_m, 'ml_m', regressor=True, classifier=True) - self._learner = {'ml_l': ml_l, 'ml_m': ml_m} + _ = self._check_learner(ml_l, "ml_l", regressor=True, classifier=False) + ml_m_is_classifier = self._check_learner(ml_m, "ml_m", regressor=True, classifier=True) + self._learner = {"ml_l": ml_l, "ml_m": ml_m} if ml_g is not None: - if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner['ml_g'] = ml_g + if (isinstance(self.score, str) & (self.score == "IV-type")) | callable(self.score): + _ = self._check_learner(ml_g, "ml_g", regressor=True, classifier=False) + self._learner["ml_g"] = ml_g else: - assert (isinstance(self.score, str) & (self.score == 'partialling out')) - warnings.warn(('A learner ml_g has been provided for score = "partialling out" but will be ignored. "' - 'A learner ml_g is not required for estimation.')) - elif isinstance(self.score, str) & (self.score == 'IV-type'): - warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. " - "Set ml_g = clone(ml_l).")) - self._learner['ml_g'] = clone(ml_l) - - self._predict_method = {'ml_l': 'predict'} - if 'ml_g' in self._learner: - self._predict_method['ml_g'] = 'predict' + assert isinstance(self.score, str) & (self.score == "partialling out") + warnings.warn( + ( + 'A learner ml_g has been provided for score = "partialling out" but will be ignored. "' + "A learner ml_g is not required for estimation." + ) + ) + elif isinstance(self.score, str) & (self.score == "IV-type"): + warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. " "Set ml_g = clone(ml_l).")) + self._learner["ml_g"] = clone(ml_l) + + self._predict_method = {"ml_l": "predict"} + if "ml_g" in self._learner: + self._predict_method["ml_g"] = "predict" if ml_m_is_classifier: if self._dml_data.binary_treats.all(): - self._predict_method['ml_m'] = 'predict_proba' + self._predict_method["ml_m"] = "predict_proba" else: - raise ValueError(f'The ml_m learner {str(ml_m)} was identified as classifier ' - 'but at least one treatment variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_m learner {str(ml_m)} was identified as classifier " + "but at least one treatment variable is not binary with values 0 and 1." + ) else: - self._predict_method['ml_m'] = 'predict' + self._predict_method["ml_m"] = "predict" self._initialize_ml_nuisance_params() self._sensitivity_implemented = True self._external_predictions_implemented = True def _initialize_ml_nuisance_params(self): - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in self._learner} + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in self._learner} def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ' - 'To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR.') + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + "To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR." + ) return def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) - m_external = external_predictions['ml_m'] is not None - l_external = external_predictions['ml_l'] is not None - if 'ml_g' in self._learner: - g_external = external_predictions['ml_g'] is not None + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) + m_external = external_predictions["ml_m"] is not None + l_external = external_predictions["ml_l"] is not None + if "ml_g" in self._learner: + g_external = external_predictions["ml_g"] is not None else: g_external = False # nuisance l if l_external: - l_hat = {'preds': external_predictions['ml_l'], - 'targets': None, - 'models': None} + l_hat = {"preds": external_predictions["ml_l"], "targets": None, "models": None} elif self._score == "IV-type" and g_external: - l_hat = {'preds': None, - 'targets': None, - 'models': None} + l_hat = {"preds": None, "targets": None, "models": None} else: - l_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_l'), method=self._predict_method['ml_l'], - return_models=return_models) - _check_finite_predictions(l_hat['preds'], self._learner['ml_l'], 'ml_l', smpls) + l_hat = _dml_cv_predict( + self._learner["ml_l"], + x, + y, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_l"), + method=self._predict_method["ml_l"], + return_models=return_models, + ) + _check_finite_predictions(l_hat["preds"], self._learner["ml_l"], "ml_l", smpls) # nuisance m if m_external: - m_hat = {'preds': external_predictions['ml_m'], - 'targets': None, - 'models': None} + m_hat = {"preds": external_predictions["ml_m"], "targets": None, "models": None} else: - m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m'], - return_models=return_models) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) - if self._check_learner(self._learner['ml_m'], 'ml_m', regressor=True, classifier=True): - _check_is_propensity(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls, eps=1e-12) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + return_models=return_models, + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) + if self._check_learner(self._learner["ml_m"], "ml_m", regressor=True, classifier=True): + _check_is_propensity(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls, eps=1e-12) if self._dml_data.binary_treats[self._dml_data.d_cols[self._i_treat]]: - _check_binary_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', self._dml_data.d_cols[self._i_treat]) + _check_binary_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", self._dml_data.d_cols[self._i_treat]) # an estimate of g is obtained for the IV-type score and callable scores - g_hat = {'preds': None, 'targets': None, 'models': None} - if 'ml_g' in self._learner: + g_hat = {"preds": None, "targets": None, "models": None} + if "ml_g" in self._learner: # nuisance g if g_external: - g_hat = {'preds': external_predictions['ml_g'], - 'targets': None, - 'models': None} + g_hat = {"preds": external_predictions["ml_g"], "targets": None, "models": None} else: # get an initial estimate for theta using the partialling out score - psi_a = -np.multiply(d - m_hat['preds'], d - m_hat['preds']) - psi_b = np.multiply(d - m_hat['preds'], y - l_hat['preds']) + psi_a = -np.multiply(d - m_hat["preds"], d - m_hat["preds"]) + psi_b = np.multiply(d - m_hat["preds"], y - l_hat["preds"]) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) - g_hat = _dml_cv_predict(self._learner['ml_g'], x, y - theta_initial*d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g'), method=self._predict_method['ml_g'], - return_models=return_models) - _check_finite_predictions(g_hat['preds'], self._learner['ml_g'], 'ml_g', smpls) - - psi_a, psi_b = self._score_elements(y, d, l_hat['preds'], m_hat['preds'], g_hat['preds'], smpls) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_l': l_hat['preds'], - 'ml_m': m_hat['preds'], - 'ml_g': g_hat['preds']}, - 'targets': {'ml_l': l_hat['targets'], - 'ml_m': m_hat['targets'], - 'ml_g': g_hat['targets']}, - 'models': {'ml_l': l_hat['models'], - 'ml_m': m_hat['models'], - 'ml_g': g_hat['models']}} + g_hat = _dml_cv_predict( + self._learner["ml_g"], + x, + y - theta_initial * d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g"), + method=self._predict_method["ml_g"], + return_models=return_models, + ) + _check_finite_predictions(g_hat["preds"], self._learner["ml_g"], "ml_g", smpls) + + psi_a, psi_b = self._score_elements(y, d, l_hat["preds"], m_hat["preds"], g_hat["preds"], smpls) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_l": l_hat["preds"], "ml_m": m_hat["preds"], "ml_g": g_hat["preds"]}, + "targets": {"ml_l": l_hat["targets"], "ml_m": m_hat["targets"], "ml_g": g_hat["targets"]}, + "models": {"ml_l": l_hat["models"], "ml_m": m_hat["models"], "ml_g": g_hat["models"]}, + } return psi_elements, preds @@ -236,19 +237,17 @@ def _score_elements(self, y, d, l_hat, m_hat, g_hat, smpls): v_hat = d - m_hat if isinstance(self.score, str): - if self.score == 'IV-type': - psi_a = - np.multiply(v_hat, d) + if self.score == "IV-type": + psi_a = -np.multiply(v_hat, d) psi_b = np.multiply(v_hat, y - g_hat) else: - assert self.score == 'partialling out' + assert self.score == "partialling out" u_hat = y - l_hat psi_a = -np.multiply(v_hat, v_hat) psi_b = np.multiply(v_hat, u_hat) else: assert callable(self.score) - psi_a, psi_b = self.score(y=y, d=d, - l_hat=l_hat, m_hat=m_hat, g_hat=g_hat, - smpls=smpls) + psi_a, psi_b = self.score(y=y, d=d, l_hat=l_hat, m_hat=m_hat, g_hat=g_hat, smpls=smpls) return psi_a, psi_b @@ -257,15 +256,15 @@ def _sensitivity_element_est(self, preds): y = self._dml_data.y d = self._dml_data.d - m_hat = preds['predictions']['ml_m'] + m_hat = preds["predictions"]["ml_m"] theta = self.all_coef[self._i_treat, self._i_rep] - if self.score == 'partialling out': - l_hat = preds['predictions']['ml_l'] - sigma2_score_element = np.square(y - l_hat - np.multiply(theta, d-m_hat)) + if self.score == "partialling out": + l_hat = preds["predictions"]["ml_l"] + sigma2_score_element = np.square(y - l_hat - np.multiply(theta, d - m_hat)) else: - assert self.score == 'IV-type' - g_hat = preds['predictions']['ml_g'] + assert self.score == "IV-type" + g_hat = preds["predictions"]["ml_g"] sigma2_score_element = np.square(y - g_hat - np.multiply(theta, d)) sigma2 = np.mean(sigma2_score_element) @@ -276,39 +275,55 @@ def _sensitivity_element_est(self, preds): psi_nu2 = nu2 - np.multiply(np.square(treatment_residual), np.square(nu2)) rr = np.multiply(treatment_residual, nu2) - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2, - 'riesz_rep': rr, - } + element_dict = { + "sigma2": sigma2, + "nu2": nu2, + "psi_sigma2": psi_sigma2, + "psi_nu2": psi_nu2, + "riesz_rep": rr, + } return element_dict - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_l': None, - 'ml_m': None, - 'ml_g': None} + scoring_methods = {"ml_l": None, "ml_m": None, "ml_g": None} train_inds = [train_index for (train_index, _) in smpls] - l_tune_res = _dml_tune(y, x, train_inds, - self._learner['ml_l'], param_grids['ml_l'], scoring_methods['ml_l'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - m_tune_res = _dml_tune(d, x, train_inds, - self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + l_tune_res = _dml_tune( + y, + x, + train_inds, + self._learner["ml_l"], + param_grids["ml_l"], + scoring_methods["ml_l"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) + m_tune_res = _dml_tune( + d, + x, + train_inds, + self._learner["ml_m"], + param_grids["ml_m"], + scoring_methods["ml_m"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) l_best_params = [xx.best_params_ for xx in l_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] # an ML model for g is obtained for the IV-type score and callable scores - if 'ml_g' in self._learner: + if "ml_g" in self._learner: # construct an initial theta estimate from the tuned models using the partialling out score l_hat = np.full_like(y, np.nan) m_hat = np.full_like(d, np.nan) @@ -318,25 +333,27 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ psi_a = -np.multiply(d - m_hat, d - m_hat) psi_b = np.multiply(d - m_hat, y - l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) - g_tune_res = _dml_tune(y - theta_initial*d, x, train_inds, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) + g_tune_res = _dml_tune( + y - theta_initial * d, + x, + train_inds, + self._learner["ml_g"], + param_grids["ml_g"], + scoring_methods["ml_g"], + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) g_best_params = [xx.best_params_ for xx in g_tune_res] - params = {'ml_l': l_best_params, - 'ml_m': m_best_params, - 'ml_g': g_best_params} - tune_res = {'l_tune': l_tune_res, - 'm_tune': m_tune_res, - 'g_tune': g_tune_res} + params = {"ml_l": l_best_params, "ml_m": m_best_params, "ml_g": g_best_params} + tune_res = {"l_tune": l_tune_res, "m_tune": m_tune_res, "g_tune": g_tune_res} else: - params = {'ml_l': l_best_params, - 'ml_m': m_best_params} - tune_res = {'l_tune': l_tune_res, - 'm_tune': m_tune_res} + params = {"ml_l": l_best_params, "ml_m": m_best_params} + tune_res = {"l_tune": l_tune_res, "m_tune": m_tune_res} - res = {'params': params, - 'tune_res': tune_res} + res = {"params": params, "tune_res": tune_res} return res @@ -363,11 +380,11 @@ def cate(self, basis, is_gate=False, **kwargs): Best linear Predictor model. """ if self._dml_data.n_treat > 1: - raise NotImplementedError('Only implemented for single treatment. ' + - f'Number of treatments is {str(self._dml_data.n_treat)}.') + raise NotImplementedError( + "Only implemented for single treatment. " + f"Number of treatments is {str(self._dml_data.n_treat)}." + ) if self.n_rep != 1: - raise NotImplementedError('Only implemented for one repetition. ' + - f'Number of repetitions is {str(self.n_rep)}.') + raise NotImplementedError("Only implemented for one repetition. " + f"Number of repetitions is {str(self.n_rep)}.") Y_tilde, D_tilde = self._partial_out() @@ -401,17 +418,18 @@ def gate(self, groups, **kwargs): """ if not isinstance(groups, pd.DataFrame): - raise TypeError('Groups must be of DataFrame type. ' - f'Groups of type {str(type(groups))} was passed.') + raise TypeError("Groups must be of DataFrame type. " f"Groups of type {str(type(groups))} was passed.") if not all(groups.dtypes == bool) or all(groups.dtypes == int): if groups.shape[1] == 1: - groups = pd.get_dummies(groups, prefix='Group', prefix_sep='_') + groups = pd.get_dummies(groups, prefix="Group", prefix_sep="_") else: - raise TypeError('Columns of groups must be of bool type or int type (dummy coded). ' - 'Alternatively, groups should only contain one column.') + raise TypeError( + "Columns of groups must be of bool type or int type (dummy coded). " + "Alternatively, groups should only contain one column." + ) if any(groups.sum(0) <= 5): - warnings.warn('At least one group effect is estimated with less than 6 observations.') + warnings.warn("At least one group effect is estimated with less than 6 observations.") model = self.cate(groups, is_gate=True, **kwargs) return model @@ -429,7 +447,7 @@ def _partial_out(self): The residual of the regression of D on X. """ if self.predictions is None: - raise ValueError('predictions are None. Call .fit(store_predictions=True) to store the predictions.') + raise ValueError("predictions are None. Call .fit(store_predictions=True) to store the predictions.") y = self._dml_data.y.reshape(-1, 1) d = self._dml_data.d.reshape(-1, 1) diff --git a/doubleml/plm/tests/_utils_pliv_manual.py b/doubleml/plm/tests/_utils_pliv_manual.py index c9b6ef975..90b1e6892 100644 --- a/doubleml/plm/tests/_utils_pliv_manual.py +++ b/doubleml/plm/tests/_utils_pliv_manual.py @@ -5,9 +5,23 @@ from ...tests._utils_boot import boot_manual, draw_weights -def fit_pliv(y, x, d, z, - learner_l, learner_m, learner_r, learner_g, all_smpls, score, - n_rep=1, l_params=None, m_params=None, r_params=None, g_params=None): +def fit_pliv( + y, + x, + d, + z, + learner_l, + learner_m, + learner_r, + learner_g, + all_smpls, + score, + n_rep=1, + l_params=None, + m_params=None, + r_params=None, + g_params=None, +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -19,33 +33,38 @@ def fit_pliv(y, x, d, z, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - fit_g = (score == 'IV-type') | callable(score) - l_hat, m_hat, r_hat, g_hat = fit_nuisance_pliv(y, x, d, z, - learner_l, learner_m, learner_r, learner_g, - smpls, fit_g, - l_params, m_params, r_params, g_params) + fit_g = (score == "IV-type") | callable(score) + l_hat, m_hat, r_hat, g_hat = fit_nuisance_pliv( + y, x, d, z, learner_l, learner_m, learner_r, learner_g, smpls, fit_g, l_params, m_params, r_params, g_params + ) all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_r_hat.append(r_hat) all_g_hat.append(g_hat) - thetas[i_rep], ses[i_rep] = pliv_dml2(y, x, d, z, - l_hat, m_hat, r_hat, g_hat, - smpls, score) + thetas[i_rep], ses[i_rep] = pliv_dml2(y, x, d, z, l_hat, m_hat, r_hat, g_hat, smpls, score) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat, 'all_g_hat': all_g_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_l_hat": all_l_hat, + "all_m_hat": all_m_hat, + "all_r_hat": all_r_hat, + "all_g_hat": all_g_hat, + } return res -def fit_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, fit_g=True, - l_params=None, m_params=None, r_params=None, g_params=None): +def fit_nuisance_pliv( + y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, fit_g=True, l_params=None, m_params=None, r_params=None, g_params=None +): l_hat = fit_predict(y, x, ml_l, l_params, smpls) m_hat = fit_predict(z, x, ml_m, m_params, smpls) @@ -53,8 +72,7 @@ def fit_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, fit_g=True, r_hat = fit_predict(d, x, ml_r, r_params, smpls) if fit_g: - y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, _ = compute_pliv_residuals( - y, d, z, l_hat, m_hat, r_hat, [], smpls) + y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, _ = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, [], smpls) psi_a = -np.multiply(d_minus_r_hat, z_minus_m_hat) psi_b = np.multiply(z_minus_m_hat, y_minus_l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) @@ -67,8 +85,23 @@ def fit_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, fit_g=True, return l_hat, m_hat, r_hat, g_hat -def tune_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, n_folds_tune, - param_grid_l, param_grid_m, param_grid_r, param_grid_g, tune_g=True): +def tune_nuisance_pliv( + y, + x, + d, + z, + ml_l, + ml_m, + ml_r, + ml_g, + smpls, + n_folds_tune, + param_grid_l, + param_grid_m, + param_grid_r, + param_grid_g, + tune_g=True, +): l_tune_res = tune_grid_search(y, x, ml_l, smpls, param_grid_l, n_folds_tune) m_tune_res = tune_grid_search(z, x, ml_m, smpls, param_grid_m, n_folds_tune) @@ -87,7 +120,7 @@ def tune_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, n_folds_tune, psi_b = np.multiply(z - m_hat, y - l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) - g_tune_res = tune_grid_search(y - theta_initial*d, x, ml_g, smpls, param_grid_g, n_folds_tune) + g_tune_res = tune_grid_search(y - theta_initial * d, x, ml_g, smpls, param_grid_g, n_folds_tune) g_best_params = [xx.best_params_ for xx in g_tune_res] else: g_best_params = [] @@ -100,10 +133,10 @@ def tune_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, ml_g, smpls, n_folds_tune, def compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, g_hat, smpls): - y_minus_l_hat = np.full_like(y, np.nan, dtype='float64') - z_minus_m_hat = np.full_like(y, np.nan, dtype='float64') - d_minus_r_hat = np.full_like(d, np.nan, dtype='float64') - y_minus_g_hat = np.full_like(y, np.nan, dtype='float64') + y_minus_l_hat = np.full_like(y, np.nan, dtype="float64") + z_minus_m_hat = np.full_like(y, np.nan, dtype="float64") + d_minus_r_hat = np.full_like(d, np.nan, dtype="float64") + y_minus_g_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): y_minus_l_hat[test_index] = y[test_index] - l_hat[idx] z_minus_m_hat[test_index] = z[test_index] - m_hat[idx] @@ -117,7 +150,8 @@ def compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, g_hat, smpls): def pliv_dml2(y, x, d, z, l_hat, m_hat, r_hat, g_hat, smpls, score): n_obs = len(y) y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat = compute_pliv_residuals( - y, d, z, l_hat, m_hat, r_hat, g_hat, smpls) + y, d, z, l_hat, m_hat, r_hat, g_hat, smpls + ) theta_hat = pliv_orth(y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat, d, score) se = np.sqrt(var_pliv(theta_hat, d, y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat, score, n_obs)) @@ -125,30 +159,54 @@ def pliv_dml2(y, x, d, z, l_hat, m_hat, r_hat, g_hat, smpls, score): def var_pliv(theta, d, y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat, score, n_obs): - if score == 'partialling out': - var = 1/n_obs * 1/np.power(np.mean(np.multiply(z_minus_m_hat, d_minus_r_hat)), 2) * \ - np.mean(np.power(np.multiply(y_minus_l_hat - d_minus_r_hat*theta, z_minus_m_hat), 2)) + if score == "partialling out": + var = ( + 1 + / n_obs + * 1 + / np.power(np.mean(np.multiply(z_minus_m_hat, d_minus_r_hat)), 2) + * np.mean(np.power(np.multiply(y_minus_l_hat - d_minus_r_hat * theta, z_minus_m_hat), 2)) + ) else: - assert score == 'IV-type' - var = 1/n_obs * 1/np.power(np.mean(np.multiply(z_minus_m_hat, d)), 2) * \ - np.mean(np.power(np.multiply(y_minus_g_hat - d*theta, z_minus_m_hat), 2)) + assert score == "IV-type" + var = ( + 1 + / n_obs + * 1 + / np.power(np.mean(np.multiply(z_minus_m_hat, d)), 2) + * np.mean(np.power(np.multiply(y_minus_g_hat - d * theta, z_minus_m_hat), 2)) + ) return var def pliv_orth(y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat, d, score): - if score == 'partialling out': - res = np.mean(np.multiply(z_minus_m_hat, y_minus_l_hat))/np.mean(np.multiply(z_minus_m_hat, d_minus_r_hat)) + if score == "partialling out": + res = np.mean(np.multiply(z_minus_m_hat, y_minus_l_hat)) / np.mean(np.multiply(z_minus_m_hat, d_minus_r_hat)) else: - assert score == 'IV-type' - res = np.mean(np.multiply(z_minus_m_hat, y_minus_g_hat))/np.mean(np.multiply(z_minus_m_hat, d)) + assert score == "IV-type" + res = np.mean(np.multiply(z_minus_m_hat, y_minus_g_hat)) / np.mean(np.multiply(z_minus_m_hat, d)) return res -def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, all_g_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1, apply_cross_fitting=True): +def boot_pliv( + y, + d, + z, + thetas, + ses, + all_l_hat, + all_m_hat, + all_r_hat, + all_g_hat, + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep=1, + apply_cross_fitting=True, +): all_boot_t_stat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] @@ -159,8 +217,21 @@ def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, all_g_hat, n_obs = len(test_index) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_pliv_single_split( - thetas[i_rep], y, d, z, all_l_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], all_g_hat[i_rep], smpls, - score, ses[i_rep], weights, n_rep_boot, apply_cross_fitting) + thetas[i_rep], + y, + d, + z, + all_l_hat[i_rep], + all_m_hat[i_rep], + all_r_hat[i_rep], + all_g_hat[i_rep], + smpls, + score, + ses[i_rep], + weights, + n_rep_boot, + apply_cross_fitting, + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -168,30 +239,32 @@ def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, all_g_hat, return boot_t_stat -def boot_pliv_single_split(theta, y, d, z, l_hat, m_hat, r_hat, g_hat, - smpls, score, se, weights, n_rep_boot, apply_cross_fitting): +def boot_pliv_single_split( + theta, y, d, z, l_hat, m_hat, r_hat, g_hat, smpls, score, se, weights, n_rep_boot, apply_cross_fitting +): y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat = compute_pliv_residuals( - y, d, z, l_hat, m_hat, r_hat, g_hat, smpls) + y, d, z, l_hat, m_hat, r_hat, g_hat, smpls + ) if apply_cross_fitting: - if score == 'partialling out': + if score == "partialling out": J = np.mean(-np.multiply(z_minus_m_hat, d_minus_r_hat)) else: - assert score == 'IV-type' + assert score == "IV-type" J = np.mean(-np.multiply(z_minus_m_hat, d)) else: test_index = smpls[0][1] - if score == 'partialling out': + if score == "partialling out": J = np.mean(-np.multiply(z_minus_m_hat[test_index], d_minus_r_hat[test_index])) else: - assert score == 'IV-type' + assert score == "IV-type" J = np.mean(-np.multiply(z_minus_m_hat[test_index], d[test_index])) - if score == 'partialling out': - psi = np.multiply(y_minus_l_hat - d_minus_r_hat*theta, z_minus_m_hat) + if score == "partialling out": + psi = np.multiply(y_minus_l_hat - d_minus_r_hat * theta, z_minus_m_hat) else: - assert score == 'IV-type' - psi = np.multiply(y_minus_g_hat - d*theta, z_minus_m_hat) + assert score == "IV-type" + psi = np.multiply(y_minus_g_hat - d * theta, z_minus_m_hat) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep_boot, apply_cross_fitting) diff --git a/doubleml/plm/tests/_utils_pliv_partial_x_manual.py b/doubleml/plm/tests/_utils_pliv_partial_x_manual.py index fe01a58bc..6fa64a14d 100644 --- a/doubleml/plm/tests/_utils_pliv_partial_x_manual.py +++ b/doubleml/plm/tests/_utils_pliv_partial_x_manual.py @@ -5,9 +5,9 @@ from ...tests._utils_boot import boot_manual, draw_weights -def fit_pliv_partial_x(y, x, d, z, - learner_l, learner_m, learner_r, all_smpls, score, - n_rep=1, l_params=None, m_params=None, r_params=None): +def fit_pliv_partial_x( + y, x, d, z, learner_l, learner_m, learner_r, all_smpls, score, n_rep=1, l_params=None, m_params=None, r_params=None +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -18,25 +18,28 @@ def fit_pliv_partial_x(y, x, d, z, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - l_hat, m_hat, r_hat = fit_nuisance_pliv_partial_x(y, x, d, z, - learner_l, learner_m, learner_r, - smpls, - l_params, m_params, r_params) + l_hat, m_hat, r_hat = fit_nuisance_pliv_partial_x( + y, x, d, z, learner_l, learner_m, learner_r, smpls, l_params, m_params, r_params + ) all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_r_hat.append(r_hat) - thetas[i_rep], ses[i_rep] = pliv_partial_x_dml2(y, x, d, z, - l_hat, m_hat, r_hat, - smpls, score) + thetas[i_rep], ses[i_rep] = pliv_partial_x_dml2(y, x, d, z, l_hat, m_hat, r_hat, smpls, score) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_l_hat": all_l_hat, + "all_m_hat": all_m_hat, + "all_r_hat": all_r_hat, + } return res @@ -54,8 +57,8 @@ def fit_nuisance_pliv_partial_x(y, x, d, z, ml_l, ml_m, ml_r, smpls, l_params=No r_hat = fit_predict(d, x, ml_r, r_params, smpls) - r_hat_array = np.zeros_like(d, dtype='float64') - m_hat_array = np.zeros_like(z, dtype='float64') + r_hat_array = np.zeros_like(d, dtype="float64") + m_hat_array = np.zeros_like(z, dtype="float64") for idx, (_, test_index) in enumerate(smpls): r_hat_array[test_index] = r_hat[idx] for i_instr in range(z.shape[1]): @@ -66,8 +69,7 @@ def fit_nuisance_pliv_partial_x(y, x, d, z, ml_l, ml_m, ml_r, smpls, l_params=No return l_hat, r_hat, r_hat_tilde -def tune_nuisance_pliv_partial_x(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_tune, - param_grid_l, param_grid_m, param_grid_r): +def tune_nuisance_pliv_partial_x(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_tune, param_grid_l, param_grid_m, param_grid_r): l_tune_res = tune_grid_search(y, x, ml_l, smpls, param_grid_l, n_folds_tune) m_tune_res = list() @@ -84,8 +86,8 @@ def tune_nuisance_pliv_partial_x(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_tu def compute_pliv_partial_x_residuals(y, d, l_hat, r_hat, smpls): - u_hat = np.full_like(y, np.nan, dtype='float64') - w_hat = np.full_like(y, np.nan, dtype='float64') + u_hat = np.full_like(y, np.nan, dtype="float64") + w_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): u_hat[test_index] = y[test_index] - l_hat[idx] w_hat[test_index] = d[test_index] - r_hat[idx] @@ -103,30 +105,46 @@ def pliv_partial_x_dml2(y, x, d, z, l_hat, r_hat, r_hat_tilde, smpls, score): def var_pliv_partial_x(theta, d, u_hat, w_hat, r_hat_tilde, score, n_obs): - assert score == 'partialling out' - var = 1/n_obs * 1/np.power(np.mean(np.multiply(r_hat_tilde, w_hat)), 2) * \ - np.mean(np.power(np.multiply(u_hat - w_hat*theta, r_hat_tilde), 2)) + assert score == "partialling out" + var = ( + 1 + / n_obs + * 1 + / np.power(np.mean(np.multiply(r_hat_tilde, w_hat)), 2) + * np.mean(np.power(np.multiply(u_hat - w_hat * theta, r_hat_tilde), 2)) + ) return var def pliv_partial_x_orth(u_hat, w_hat, r_hat_tilde, d, score): - assert score == 'partialling out' - res = np.mean(np.multiply(r_hat_tilde, u_hat))/np.mean(np.multiply(r_hat_tilde, w_hat)) + assert score == "partialling out" + res = np.mean(np.multiply(r_hat_tilde, u_hat)) / np.mean(np.multiply(r_hat_tilde, w_hat)) return res -def boot_pliv_partial_x(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1): +def boot_pliv_partial_x( + y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=1 +): all_boot_t_stat = list() for i_rep in range(n_rep): n_obs = len(y) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_pliv_partial_x_single_split( - thetas[i_rep], y, d, z, all_l_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], all_smpls[i_rep], - score, ses[i_rep], weights, n_rep_boot) + thetas[i_rep], + y, + d, + z, + all_l_hat[i_rep], + all_m_hat[i_rep], + all_r_hat[i_rep], + all_smpls[i_rep], + score, + ses[i_rep], + weights, + n_rep_boot, + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -134,14 +152,13 @@ def boot_pliv_partial_x(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, return boot_t_stat -def boot_pliv_partial_x_single_split(theta, y, d, z, l_hat, r_hat, r_hat_tilde, - smpls, score, se, weights, n_rep_boot): - assert score == 'partialling out' +def boot_pliv_partial_x_single_split(theta, y, d, z, l_hat, r_hat, r_hat_tilde, smpls, score, se, weights, n_rep_boot): + assert score == "partialling out" u_hat, w_hat = compute_pliv_partial_x_residuals(y, d, l_hat, r_hat, smpls) J = np.mean(-np.multiply(r_hat_tilde, w_hat)) - psi = np.multiply(u_hat - w_hat*theta, r_hat_tilde) + psi = np.multiply(u_hat - w_hat * theta, r_hat_tilde) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep_boot) diff --git a/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py b/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py index aa8cb2a53..4f5419c00 100644 --- a/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py +++ b/doubleml/plm/tests/_utils_pliv_partial_xz_manual.py @@ -5,9 +5,9 @@ from ...tests._utils_boot import boot_manual, draw_weights -def fit_pliv_partial_xz(y, x, d, z, - learner_l, learner_m, learner_r, all_smpls, score, - n_rep=1, l_params=None, m_params=None, r_params=None): +def fit_pliv_partial_xz( + y, x, d, z, learner_l, learner_m, learner_r, all_smpls, score, n_rep=1, l_params=None, m_params=None, r_params=None +): n_obs = len(y) thetas = np.zeros(n_rep) @@ -18,25 +18,28 @@ def fit_pliv_partial_xz(y, x, d, z, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - l_hat, m_hat, r_hat = fit_nuisance_pliv_partial_xz(y, x, d, z, - learner_l, learner_m, learner_r, - smpls, - l_params, m_params, r_params) + l_hat, m_hat, r_hat = fit_nuisance_pliv_partial_xz( + y, x, d, z, learner_l, learner_m, learner_r, smpls, l_params, m_params, r_params + ) all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_r_hat.append(r_hat) - thetas[i_rep], ses[i_rep] = pliv_partial_xz_dml2(y, x, d, z, - l_hat, m_hat, r_hat, - smpls, score) + thetas[i_rep], ses[i_rep] = pliv_partial_xz_dml2(y, x, d, z, l_hat, m_hat, r_hat, smpls, score) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_l_hat": all_l_hat, + "all_m_hat": all_m_hat, + "all_r_hat": all_r_hat, + } return res @@ -63,8 +66,7 @@ def fit_nuisance_pliv_partial_xz(y, x, d, z, ml_l, ml_m, ml_r, smpls, l_params=N return l_hat, m_hat, m_hat_tilde -def tune_nuisance_pliv_partial_xz(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_tune, - param_grid_l, param_grid_m, param_grid_r): +def tune_nuisance_pliv_partial_xz(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_tune, param_grid_l, param_grid_m, param_grid_r): l_tune_res = tune_grid_search(y, x, ml_l, smpls, param_grid_l, n_folds_tune) xz = np.hstack((x, z)) @@ -74,8 +76,7 @@ def tune_nuisance_pliv_partial_xz(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_t for idx, (train_index, _) in enumerate(smpls): m_hat = m_tune_res[idx].predict(xz[train_index, :]) r_tune_resampling = KFold(n_splits=n_folds_tune, shuffle=True) - r_grid_search = GridSearchCV(ml_r, param_grid_r, - cv=r_tune_resampling) + r_grid_search = GridSearchCV(ml_r, param_grid_r, cv=r_tune_resampling) r_tune_res[idx] = r_grid_search.fit(x[train_index, :], m_hat) l_best_params = [xx.best_params_ for xx in l_tune_res] @@ -86,9 +87,9 @@ def tune_nuisance_pliv_partial_xz(y, x, d, z, ml_l, ml_m, ml_r, smpls, n_folds_t def compute_pliv_partial_xz_residuals(y, d, l_hat, m_hat, m_hat_tilde, smpls): - u_hat = np.full_like(y, np.nan, dtype='float64') - v_hat = np.full_like(y, np.nan, dtype='float64') - w_hat = np.full_like(y, np.nan, dtype='float64') + u_hat = np.full_like(y, np.nan, dtype="float64") + v_hat = np.full_like(y, np.nan, dtype="float64") + w_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): u_hat[test_index] = y[test_index] - l_hat[idx] v_hat[test_index] = m_hat[idx] - m_hat_tilde[idx] @@ -107,30 +108,46 @@ def pliv_partial_xz_dml2(y, x, d, z, l_hat, m_hat, m_hat_tilde, smpls, score): def var_pliv_partial_xz(theta, d, u_hat, v_hat, w_hat, score, n_obs): - assert score == 'partialling out' - var = 1/n_obs * 1/np.power(np.mean(np.multiply(v_hat, w_hat)), 2) * \ - np.mean(np.power(np.multiply(u_hat - w_hat*theta, v_hat), 2)) + assert score == "partialling out" + var = ( + 1 + / n_obs + * 1 + / np.power(np.mean(np.multiply(v_hat, w_hat)), 2) + * np.mean(np.power(np.multiply(u_hat - w_hat * theta, v_hat), 2)) + ) return var def pliv_partial_xz_orth(u_hat, v_hat, w_hat, d, score): - assert score == 'partialling out' - res = np.mean(np.multiply(v_hat, u_hat))/np.mean(np.multiply(v_hat, w_hat)) + assert score == "partialling out" + res = np.mean(np.multiply(v_hat, u_hat)) / np.mean(np.multiply(v_hat, w_hat)) return res -def boot_pliv_partial_xz(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1): +def boot_pliv_partial_xz( + y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=1 +): all_boot_t_stat = list() for i_rep in range(n_rep): n_obs = len(y) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_pliv_partial_xz_single_split( - thetas[i_rep], y, d, z, all_l_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], all_smpls[i_rep], - score, ses[i_rep], weights, n_rep_boot) + thetas[i_rep], + y, + d, + z, + all_l_hat[i_rep], + all_m_hat[i_rep], + all_r_hat[i_rep], + all_smpls[i_rep], + score, + ses[i_rep], + weights, + n_rep_boot, + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -138,14 +155,13 @@ def boot_pliv_partial_xz(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, return boot_t_stat -def boot_pliv_partial_xz_single_split(theta, y, d, z, l_hat, m_hat, m_hat_tilde, - smpls, score, se, weights, n_rep_boot): - assert score == 'partialling out' +def boot_pliv_partial_xz_single_split(theta, y, d, z, l_hat, m_hat, m_hat_tilde, smpls, score, se, weights, n_rep_boot): + assert score == "partialling out" u_hat, v_hat, w_hat = compute_pliv_partial_xz_residuals(y, d, l_hat, m_hat, m_hat_tilde, smpls) J = np.mean(-np.multiply(v_hat, w_hat)) - psi = np.multiply(u_hat - w_hat*theta, v_hat) + psi = np.multiply(u_hat - w_hat * theta, v_hat) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep_boot) diff --git a/doubleml/plm/tests/_utils_pliv_partial_z_manual.py b/doubleml/plm/tests/_utils_pliv_partial_z_manual.py index 6f88fe991..839ddeea9 100644 --- a/doubleml/plm/tests/_utils_pliv_partial_z_manual.py +++ b/doubleml/plm/tests/_utils_pliv_partial_z_manual.py @@ -4,9 +4,7 @@ from ...tests._utils_boot import boot_manual, draw_weights -def fit_pliv_partial_z(y, x, d, z, - learner_r, all_smpls, score, - n_rep=1, r_params=None): +def fit_pliv_partial_z(y, x, d, z, learner_r, all_smpls, score, n_rep=1, r_params=None): n_obs = len(y) thetas = np.zeros(n_rep) @@ -15,23 +13,16 @@ def fit_pliv_partial_z(y, x, d, z, for i_rep in range(n_rep): smpls = all_smpls[i_rep] - r_hat = fit_nuisance_pliv_partial_z(y, x, d, z, - learner_r, - smpls, - r_params) + r_hat = fit_nuisance_pliv_partial_z(y, x, d, z, learner_r, smpls, r_params) all_r_hat.append(r_hat) - thetas[i_rep], ses[i_rep] = pliv_partial_z_dml2(y, x, d, z, - r_hat, - smpls, score) + thetas[i_rep], ses[i_rep] = pliv_partial_z_dml2(y, x, d, z, r_hat, smpls, score) theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_r_hat': all_r_hat} + res = {"theta": theta, "se": se, "thetas": thetas, "ses": ses, "all_r_hat": all_r_hat} return res @@ -53,7 +44,7 @@ def tune_nuisance_pliv_partial_z(y, x, d, z, ml_r, smpls, n_folds_tune, param_gr def compute_pliv_partial_z_residuals(y, r_hat, smpls): - r_hat_array = np.full_like(y, np.nan, dtype='float64') + r_hat_array = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): r_hat_array[test_index] = r_hat[idx] return r_hat_array @@ -69,30 +60,27 @@ def pliv_partial_z_dml2(y, x, d, z, r_hat, smpls, score): def var_pliv_partial_z(theta, r_hat, y, d, score, n_obs): - assert score == 'partialling out' - var = 1/n_obs * 1/np.power(np.mean(np.multiply(r_hat, d)), 2) * \ - np.mean(np.power(np.multiply(y - d*theta, r_hat), 2)) + assert score == "partialling out" + var = 1 / n_obs * 1 / np.power(np.mean(np.multiply(r_hat, d)), 2) * np.mean(np.power(np.multiply(y - d * theta, r_hat), 2)) return var def pliv_partial_z_orth(r_hat, y, d, score): - assert score == 'partialling out' - res = np.mean(np.multiply(r_hat, y))/np.mean(np.multiply(r_hat, d)) + assert score == "partialling out" + res = np.mean(np.multiply(r_hat, y)) / np.mean(np.multiply(r_hat, d)) return res -def boot_pliv_partial_z(y, d, z, thetas, ses, all_r_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1): +def boot_pliv_partial_z(y, d, z, thetas, ses, all_r_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=1): all_boot_t_stat = list() for i_rep in range(n_rep): n_obs = len(y) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_pliv_partial_z_single_split( - thetas[i_rep], y, d, z, all_r_hat[i_rep], all_smpls[i_rep], - score, ses[i_rep], weights, n_rep_boot) + thetas[i_rep], y, d, z, all_r_hat[i_rep], all_smpls[i_rep], score, ses[i_rep], weights, n_rep_boot + ) all_boot_t_stat.append(boot_t_stat) boot_t_stat = np.hstack(all_boot_t_stat) @@ -100,14 +88,13 @@ def boot_pliv_partial_z(y, d, z, thetas, ses, all_r_hat, return boot_t_stat -def boot_pliv_partial_z_single_split(theta, y, d, z, r_hat, - smpls, score, se, weights, n_rep_boot): - assert score == 'partialling out' +def boot_pliv_partial_z_single_split(theta, y, d, z, r_hat, smpls, score, se, weights, n_rep_boot): + assert score == "partialling out" r_hat_array = compute_pliv_partial_z_residuals(y, r_hat, smpls) J = np.mean(-np.multiply(r_hat_array, d)) - psi = np.multiply(y - d*theta, r_hat_array) + psi = np.multiply(y - d * theta, r_hat_array) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep_boot) diff --git a/doubleml/plm/tests/_utils_plr_manual.py b/doubleml/plm/tests/_utils_plr_manual.py index dbc820601..41572289c 100644 --- a/doubleml/plm/tests/_utils_plr_manual.py +++ b/doubleml/plm/tests/_utils_plr_manual.py @@ -6,9 +6,21 @@ from ...tests._utils_boot import boot_manual, draw_weights -def fit_plr_multitreat(y, x, d, learner_l, learner_m, learner_g, all_smpls, score, - n_rep=1, l_params=None, m_params=None, g_params=None, - use_other_treat_as_covariate=True): +def fit_plr_multitreat( + y, + x, + d, + learner_l, + learner_m, + learner_g, + all_smpls, + score, + n_rep=1, + l_params=None, + m_params=None, + g_params=None, + use_other_treat_as_covariate=True, +): n_obs = len(y) n_d = d.shape[1] @@ -32,10 +44,8 @@ def fit_plr_multitreat(y, x, d, learner_l, learner_m, learner_g, all_smpls, scor xd = x l_hat, m_hat, g_hat, thetas_this_rep[i_d], ses_this_rep[i_d] = fit_plr_single_split( - y, xd, d[:, i_d], - learner_l, learner_m, learner_g, - smpls, score, - l_params, m_params, g_params) + y, xd, d[:, i_d], learner_l, learner_m, learner_g, smpls, score, l_params, m_params, g_params + ) all_l_hat_this_rep.append(l_hat) all_m_hat_this_rep.append(m_hat) all_g_hat_this_rep.append(g_hat) @@ -54,15 +64,20 @@ def fit_plr_multitreat(y, x, d, learner_l, learner_m, learner_g, all_smpls, scor theta[i_d] = np.median(theta_vec) se[i_d] = np.sqrt(np.median(np.power(se_vec, 2) * n_obs + np.power(theta_vec - theta[i_d], 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_g_hat': all_g_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_l_hat": all_l_hat, + "all_m_hat": all_m_hat, + "all_g_hat": all_g_hat, + } return res -def fit_plr(y, x, d, learner_l, learner_m, learner_g, all_smpls, score, - n_rep=1, l_params=None, m_params=None, g_params=None): +def fit_plr(y, x, d, learner_l, learner_m, learner_g, all_smpls, score, n_rep=1, l_params=None, m_params=None, g_params=None): n_obs = len(y) thetas = np.zeros(n_rep) @@ -73,10 +88,8 @@ def fit_plr(y, x, d, learner_l, learner_m, learner_g, all_smpls, score, for i_rep in range(n_rep): smpls = all_smpls[i_rep] l_hat, m_hat, g_hat, thetas[i_rep], ses[i_rep] = fit_plr_single_split( - y, x, d, - learner_l, learner_m, learner_g, - smpls, score, - l_params, m_params, g_params) + y, x, d, learner_l, learner_m, learner_g, smpls, score, l_params, m_params, g_params + ) all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_g_hat.append(g_hat) @@ -84,35 +97,36 @@ def fit_plr(y, x, d, learner_l, learner_m, learner_g, all_smpls, score, theta = np.median(thetas) se = np.sqrt(np.median(np.power(ses, 2) * n_obs + np.power(thetas - theta, 2)) / n_obs) - res = {'theta': theta, 'se': se, - 'thetas': thetas, 'ses': ses, - 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_g_hat': all_g_hat} + res = { + "theta": theta, + "se": se, + "thetas": thetas, + "ses": ses, + "all_l_hat": all_l_hat, + "all_m_hat": all_m_hat, + "all_g_hat": all_g_hat, + } return res -def fit_plr_single_split(y, x, d, learner_l, learner_m, learner_g, smpls, score, - l_params=None, m_params=None, g_params=None): - fit_g = (score == 'IV-type') | callable(score) +def fit_plr_single_split(y, x, d, learner_l, learner_m, learner_g, smpls, score, l_params=None, m_params=None, g_params=None): + fit_g = (score == "IV-type") | callable(score) if is_classifier(learner_m): - l_hat, m_hat, g_hat = fit_nuisance_plr_classifier(y, x, d, - learner_l, learner_m, learner_g, - smpls, fit_g, - l_params, m_params, g_params) + l_hat, m_hat, g_hat = fit_nuisance_plr_classifier( + y, x, d, learner_l, learner_m, learner_g, smpls, fit_g, l_params, m_params, g_params + ) else: - l_hat, m_hat, g_hat = fit_nuisance_plr(y, x, d, - learner_l, learner_m, learner_g, - smpls, fit_g, - l_params, m_params, g_params) + l_hat, m_hat, g_hat = fit_nuisance_plr( + y, x, d, learner_l, learner_m, learner_g, smpls, fit_g, l_params, m_params, g_params + ) - theta, se = plr_dml2(y, x, d, l_hat, m_hat, g_hat, - smpls, score) + theta, se = plr_dml2(y, x, d, l_hat, m_hat, g_hat, smpls, score) return l_hat, m_hat, g_hat, theta.item(), se.item() -def fit_nuisance_plr(y, x, d, learner_l, learner_m, learner_g, smpls, fit_g=True, - l_params=None, m_params=None, g_params=None): +def fit_nuisance_plr(y, x, d, learner_l, learner_m, learner_g, smpls, fit_g=True, l_params=None, m_params=None, g_params=None): ml_l = clone(learner_l) l_hat = fit_predict(y, x, ml_l, l_params, smpls) @@ -126,15 +140,16 @@ def fit_nuisance_plr(y, x, d, learner_l, learner_m, learner_g, smpls, fit_g=True theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) ml_g = clone(learner_g) - g_hat = fit_predict(y - theta_initial*d, x, ml_g, g_params, smpls) + g_hat = fit_predict(y - theta_initial * d, x, ml_g, g_params, smpls) else: g_hat = [] return l_hat, m_hat, g_hat -def fit_nuisance_plr_classifier(y, x, d, learner_l, learner_m, learner_g, smpls, fit_g=True, - l_params=None, m_params=None, g_params=None): +def fit_nuisance_plr_classifier( + y, x, d, learner_l, learner_m, learner_g, smpls, fit_g=True, l_params=None, m_params=None, g_params=None +): ml_l = clone(learner_l) l_hat = fit_predict(y, x, ml_l, l_params, smpls) @@ -148,7 +163,7 @@ def fit_nuisance_plr_classifier(y, x, d, learner_l, learner_m, learner_g, smpls, theta_initial = -np.mean(psi_b) / np.mean(psi_a) ml_g = clone(learner_g) - g_hat = fit_predict(y - theta_initial*d, x, ml_g, g_params, smpls) + g_hat = fit_predict(y - theta_initial * d, x, ml_g, g_params, smpls) else: g_hat = [] @@ -170,7 +185,7 @@ def tune_nuisance_plr(y, x, d, ml_l, ml_m, ml_g, smpls, n_folds_tune, param_grid psi_b = np.multiply(d - m_hat, y - l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) - g_tune_res = tune_grid_search(y - theta_initial*d, x, ml_g, smpls, param_grid_g, n_folds_tune) + g_tune_res = tune_grid_search(y - theta_initial * d, x, ml_g, smpls, param_grid_g, n_folds_tune) g_best_params = [xx.best_params_ for xx in g_tune_res] else: g_best_params = [] @@ -182,9 +197,9 @@ def tune_nuisance_plr(y, x, d, ml_l, ml_m, ml_g, smpls, n_folds_tune, param_grid def compute_plr_residuals(y, d, l_hat, m_hat, g_hat, smpls): - y_minus_l_hat = np.full_like(y, np.nan, dtype='float64') - d_minus_m_hat = np.full_like(d, np.nan, dtype='float64') - y_minus_g_hat = np.full_like(y, np.nan, dtype='float64') + y_minus_l_hat = np.full_like(y, np.nan, dtype="float64") + d_minus_m_hat = np.full_like(d, np.nan, dtype="float64") + y_minus_g_hat = np.full_like(y, np.nan, dtype="float64") for idx, (_, test_index) in enumerate(smpls): y_minus_l_hat[test_index] = y[test_index] - l_hat[idx] if len(g_hat) > 0: @@ -203,30 +218,52 @@ def plr_dml2(y, x, d, l_hat, m_hat, g_hat, smpls, score): def var_plr(theta, d, y_minus_l_hat, d_minus_m_hat, y_minus_g_hat, score, n_obs): - if score == 'partialling out': - var = 1/n_obs * 1/np.power(np.mean(np.multiply(d_minus_m_hat, d_minus_m_hat)), 2) * \ - np.mean(np.power(np.multiply(y_minus_l_hat - d_minus_m_hat*theta, d_minus_m_hat), 2)) + if score == "partialling out": + var = ( + 1 + / n_obs + * 1 + / np.power(np.mean(np.multiply(d_minus_m_hat, d_minus_m_hat)), 2) + * np.mean(np.power(np.multiply(y_minus_l_hat - d_minus_m_hat * theta, d_minus_m_hat), 2)) + ) else: - assert score == 'IV-type' - var = 1/n_obs * 1/np.power(np.mean(np.multiply(d_minus_m_hat, d)), 2) * \ - np.mean(np.power(np.multiply(y_minus_g_hat - d*theta, d_minus_m_hat), 2)) + assert score == "IV-type" + var = ( + 1 + / n_obs + * 1 + / np.power(np.mean(np.multiply(d_minus_m_hat, d)), 2) + * np.mean(np.power(np.multiply(y_minus_g_hat - d * theta, d_minus_m_hat), 2)) + ) return var def plr_orth(y_minus_l_hat, d_minus_m_hat, y_minus_g_hat, d, score): - if score == 'IV-type': - res = np.mean(np.multiply(d_minus_m_hat, y_minus_g_hat))/np.mean(np.multiply(d_minus_m_hat, d)) + if score == "IV-type": + res = np.mean(np.multiply(d_minus_m_hat, y_minus_g_hat)) / np.mean(np.multiply(d_minus_m_hat, d)) else: - assert score == 'partialling out' + assert score == "partialling out" res = scipy.linalg.lstsq(d_minus_m_hat.reshape(-1, 1), y_minus_l_hat)[0] return res -def boot_plr(y, d, thetas, ses, all_l_hat, all_m_hat, all_g_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1, apply_cross_fitting=True): +def boot_plr( + y, + d, + thetas, + ses, + all_l_hat, + all_m_hat, + all_g_hat, + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep=1, + apply_cross_fitting=True, +): all_boot_t_stat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] @@ -238,9 +275,19 @@ def boot_plr(y, d, thetas, ses, all_l_hat, all_m_hat, all_g_hat, weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_t_stat = boot_plr_single_split( - thetas[i_rep], y, d, all_l_hat[i_rep], all_m_hat[i_rep], all_g_hat[i_rep], smpls, - score, ses[i_rep], - weights, n_rep_boot, apply_cross_fitting) + thetas[i_rep], + y, + d, + all_l_hat[i_rep], + all_m_hat[i_rep], + all_g_hat[i_rep], + smpls, + score, + ses[i_rep], + weights, + n_rep_boot, + apply_cross_fitting, + ) all_boot_t_stat.append(boot_t_stat) # differently for plr because of n_rep_boot and multiple treatmentsa @@ -249,9 +296,21 @@ def boot_plr(y, d, thetas, ses, all_l_hat, all_m_hat, all_g_hat, return boot_t_stat -def boot_plr_multitreat(y, d, thetas, ses, all_l_hat, all_m_hat, all_g_hat, - all_smpls, score, bootstrap, n_rep_boot, - n_rep=1, apply_cross_fitting=True): +def boot_plr_multitreat( + y, + d, + thetas, + ses, + all_l_hat, + all_m_hat, + all_g_hat, + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep=1, + apply_cross_fitting=True, +): n_d = d.shape[1] all_boot_t_stat = list() for i_rep in range(n_rep): @@ -266,10 +325,19 @@ def boot_plr_multitreat(y, d, thetas, ses, all_l_hat, all_m_hat, all_g_hat, boot_t_stat = np.full((n_d, n_rep_boot), np.nan) for i_d in range(n_d): boot_t_stat[i_d, :] = boot_plr_single_split( - thetas[i_rep][i_d], y, d[:, i_d], - all_l_hat[i_rep][i_d], all_m_hat[i_rep][i_d], all_g_hat[i_rep][i_d], - smpls, score, ses[i_rep][i_d], - weights, n_rep_boot, apply_cross_fitting) + thetas[i_rep][i_d], + y, + d[:, i_d], + all_l_hat[i_rep][i_d], + all_m_hat[i_rep][i_d], + all_g_hat[i_rep][i_d], + smpls, + score, + ses[i_rep][i_d], + weights, + n_rep_boot, + apply_cross_fitting, + ) # transpose for shape (n_rep_boot, n_d) boot_t_stat = np.transpose(boot_t_stat) @@ -281,28 +349,27 @@ def boot_plr_multitreat(y, d, thetas, ses, all_l_hat, all_m_hat, all_g_hat, return boot_t_stat -def boot_plr_single_split(theta, y, d, l_hat, m_hat, g_hat, - smpls, score, se, weights, n_rep, apply_cross_fitting): +def boot_plr_single_split(theta, y, d, l_hat, m_hat, g_hat, smpls, score, se, weights, n_rep, apply_cross_fitting): y_minus_l_hat, d_minus_m_hat, y_minus_g_hat = compute_plr_residuals(y, d, l_hat, m_hat, g_hat, smpls) if apply_cross_fitting: - if score == 'partialling out': + if score == "partialling out": J = np.mean(-np.multiply(d_minus_m_hat, d_minus_m_hat)) else: - assert score == 'IV-type' + assert score == "IV-type" J = np.mean(-np.multiply(d_minus_m_hat, d)) else: test_index = smpls[0][1] - if score == 'partialling out': + if score == "partialling out": J = np.mean(-np.multiply(d_minus_m_hat[test_index], d_minus_m_hat[test_index])) else: - assert score == 'IV-type' + assert score == "IV-type" J = np.mean(-np.multiply(d_minus_m_hat[test_index], d[test_index])) - if score == 'partialling out': + if score == "partialling out": psi = np.multiply(y_minus_l_hat - d_minus_m_hat * theta, d_minus_m_hat) else: - assert score == 'IV-type' + assert score == "IV-type" psi = np.multiply(y_minus_g_hat - d * theta, d_minus_m_hat) boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep, apply_cross_fitting) @@ -322,25 +389,23 @@ def fit_sensitivity_elements_plr(y, d, all_coef, predictions, score, n_rep): for i_rep in range(n_rep): for i_treat in range(n_treat): d_tilde = d[:, i_treat] - m_hat = predictions['ml_m'][:, i_rep, i_treat] + m_hat = predictions["ml_m"][:, i_rep, i_treat] theta = all_coef[i_treat, i_rep] - if score == 'partialling out': - l_hat = predictions['ml_l'][:, i_rep, i_treat] - sigma2_score_element = np.square(y - l_hat - np.multiply(theta, d_tilde-m_hat)) + if score == "partialling out": + l_hat = predictions["ml_l"][:, i_rep, i_treat] + sigma2_score_element = np.square(y - l_hat - np.multiply(theta, d_tilde - m_hat)) else: - assert score == 'IV-type' - g_hat = predictions['ml_g'][:, i_rep, i_treat] + assert score == "IV-type" + g_hat = predictions["ml_g"][:, i_rep, i_treat] sigma2_score_element = np.square(y - g_hat - np.multiply(theta, d_tilde)) sigma2[0, i_rep, i_treat] = np.mean(sigma2_score_element) psi_sigma2[:, i_rep, i_treat] = sigma2_score_element - sigma2[0, i_rep, i_treat] - nu2[0, i_rep, i_treat] = np.divide(1.0, np.mean(np.square(d_tilde-m_hat))) - psi_nu2[:, i_rep, i_treat] = nu2[0, i_rep, i_treat] - \ - np.multiply(np.square(d_tilde-m_hat), np.square(nu2[0, i_rep, i_treat])) + nu2[0, i_rep, i_treat] = np.divide(1.0, np.mean(np.square(d_tilde - m_hat))) + psi_nu2[:, i_rep, i_treat] = nu2[0, i_rep, i_treat] - np.multiply( + np.square(d_tilde - m_hat), np.square(nu2[0, i_rep, i_treat]) + ) - element_dict = {'sigma2': sigma2, - 'nu2': nu2, - 'psi_sigma2': psi_sigma2, - 'psi_nu2': psi_nu2} + element_dict = {"sigma2": sigma2, "nu2": nu2, "psi_sigma2": psi_sigma2, "psi_nu2": psi_nu2} return element_dict diff --git a/doubleml/plm/tests/conftest.py b/doubleml/plm/tests/conftest.py index d61c8b4c4..497d6fc9d 100644 --- a/doubleml/plm/tests/conftest.py +++ b/doubleml/plm/tests/conftest.py @@ -11,7 +11,7 @@ def _g(x): return np.power(np.sin(x), 2) -def _m(x, nu=0., gamma=1.): +def _m(x, nu=0.0, gamma=1.0): return 0.5 / np.pi * (np.sinh(gamma)) / (np.cosh(gamma) - np.cos(x - nu)) @@ -19,10 +19,7 @@ def _m2(x): return np.power(x, 2) -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20), - (1000, 100)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20), (1000, 100)]) def generate_data1(request): n_p = request.param np.random.seed(1111) @@ -37,8 +34,7 @@ def generate_data1(request): return data -@pytest.fixture(scope='session', - params=[(500, 20)]) +@pytest.fixture(scope="session", params=[(500, 20)]) def generate_data2(request): n_p = request.param np.random.seed(1111) @@ -53,8 +49,7 @@ def generate_data2(request): return data -@pytest.fixture(scope='session', - params=[(1000, 20)]) +@pytest.fixture(scope="session", params=[(1000, 20)]) def generate_data_bivariate(request): n_p = request.param np.random.seed(1111) @@ -66,24 +61,44 @@ def generate_data_bivariate(request): sigma = make_spd_matrix(p) # generating data - x = np.random.multivariate_normal(np.zeros(p), sigma, size=[n, ]) + x = np.random.multivariate_normal( + np.zeros(p), + sigma, + size=[ + n, + ], + ) G = _g(np.dot(x, b)) M0 = _m(np.dot(x, b)) M1 = _m2(np.dot(x, b)) - D0 = M0 + np.random.standard_normal(size=[n, ]) - D1 = M1 + np.random.standard_normal(size=[n, ]) - y = theta[0] * D0 + theta[1] * D1 + G + np.random.standard_normal(size=[n, ]) + D0 = M0 + np.random.standard_normal( + size=[ + n, + ] + ) + D1 = M1 + np.random.standard_normal( + size=[ + n, + ] + ) + y = ( + theta[0] * D0 + + theta[1] * D1 + + G + + np.random.standard_normal( + size=[ + n, + ] + ) + ) d = np.column_stack((D0, D1)) - column_names = [f'X{i + 1}' for i in np.arange(p)] + ['y'] + \ - [f'd{i + 1}' for i in np.arange(2)] - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=column_names) + column_names = [f"X{i + 1}" for i in np.arange(p)] + ["y"] + [f"d{i + 1}" for i in np.arange(2)] + data = pd.DataFrame(np.column_stack((x, y, d)), columns=column_names) return data -@pytest.fixture(scope='session', - params=[(1000, 20)]) +@pytest.fixture(scope="session", params=[(1000, 20)]) def generate_data_toeplitz(request, betamax=4, decay=0.99, threshold=0, noisevar=10): n_p = request.param np.random.seed(3141) @@ -100,20 +115,29 @@ def generate_data_toeplitz(request, betamax=4, decay=0.99, threshold=0, noisevar mu = np.zeros(p) # generating data - x = np.random.multivariate_normal(mu, sigma, size=[n, ]) - y = np.dot(x, beta) + np.random.normal(loc=0.0, scale=np.sqrt(noisevar), size=[n, ]) + x = np.random.multivariate_normal( + mu, + sigma, + size=[ + n, + ], + ) + y = np.dot(x, beta) + np.random.normal( + loc=0.0, + scale=np.sqrt(noisevar), + size=[ + n, + ], + ) d = x[:, cols_treatment] x = np.delete(x, cols_treatment, axis=1) - column_names = [f'X{i + 1}' for i in np.arange(x.shape[1])] + \ - ['y'] + [f'd{i + 1}' for i in np.arange(len(cols_treatment))] - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=column_names) + column_names = [f"X{i + 1}" for i in np.arange(x.shape[1])] + ["y"] + [f"d{i + 1}" for i in np.arange(len(cols_treatment))] + data = pd.DataFrame(np.column_stack((x, y, d)), columns=column_names) return data -@pytest.fixture(scope='session', - params=[(1000, 20)]) +@pytest.fixture(scope="session", params=[(1000, 20)]) def generate_data_iv(request): n_p = request.param np.random.seed(1111) @@ -128,14 +152,13 @@ def generate_data_iv(request): return data -@pytest.fixture(scope='session', - params=[500]) +@pytest.fixture(scope="session", params=[500]) def generate_data_pliv_partialXZ(request): n_p = request.param np.random.seed(1111) # setting parameters n = n_p - theta = 1. + theta = 1.0 # generating data data = make_pliv_CHS2015(n, alpha=theta) @@ -143,14 +166,13 @@ def generate_data_pliv_partialXZ(request): return data -@pytest.fixture(scope='session', - params=[500]) +@pytest.fixture(scope="session", params=[500]) def generate_data_pliv_partialX(request): n_p = request.param np.random.seed(1111) # setting parameters n = n_p - theta = 1. + theta = 1.0 # generating data data = make_pliv_CHS2015(n, alpha=theta, dim_z=5) @@ -158,14 +180,13 @@ def generate_data_pliv_partialX(request): return data -@pytest.fixture(scope='session', - params=[500]) +@pytest.fixture(scope="session", params=[500]) def generate_data_pliv_partialZ(request): n_p = request.param np.random.seed(1111) # setting parameters n = n_p - theta = 1. + theta = 1.0 # generating data data = make_data_pliv_partialZ(n, alpha=theta, dim_x=5) @@ -173,26 +194,38 @@ def generate_data_pliv_partialZ(request): return data -def make_data_pliv_partialZ(n_obs, alpha=1., dim_x=5, dim_z=150): - xx = np.random.multivariate_normal(np.zeros(2), - np.array([[1., 0.6], [0.6, 1.]]), - size=[n_obs, ]) +def make_data_pliv_partialZ(n_obs, alpha=1.0, dim_x=5, dim_z=150): + xx = np.random.multivariate_normal( + np.zeros(2), + np.array([[1.0, 0.6], [0.6, 1.0]]), + size=[ + n_obs, + ], + ) epsilon = xx[:, 0] u = xx[:, 1] sigma = toeplitz([np.power(0.5, k) for k in range(1, dim_x + 1)]) - x = np.random.multivariate_normal(np.zeros(dim_x), - sigma, - size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + sigma, + size=[ + n_obs, + ], + ) I_z = np.eye(dim_z) - xi = np.random.multivariate_normal(np.zeros(dim_z), - 0.25 * I_z, - size=[n_obs, ]) - - beta = [1 / (k ** 2) for k in range(1, dim_x + 1)] + xi = np.random.multivariate_normal( + np.zeros(dim_z), + 0.25 * I_z, + size=[ + n_obs, + ], + ) + + beta = [1 / (k**2) for k in range(1, dim_x + 1)] gamma = beta - delta = [1 / (k ** 2) for k in range(1, dim_z + 1)] + delta = [1 / (k**2) for k in range(1, dim_z + 1)] I_x = np.eye(dim_x) Pi = np.hstack((I_x, np.zeros((dim_x, dim_z - dim_x)))) @@ -201,9 +234,8 @@ def make_data_pliv_partialZ(n_obs, alpha=1., dim_x=5, dim_z=150): d = np.dot(x, gamma) + np.dot(z, delta) + u y = alpha * d + np.dot(x, beta) + epsilon - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - z_cols = [f'Z{i + 1}' for i in np.arange(dim_z)] - data = pd.DataFrame(np.column_stack((x, y, d, z)), - columns=x_cols + ['y', 'd'] + z_cols) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + z_cols = [f"Z{i + 1}" for i in np.arange(dim_z)] + data = pd.DataFrame(np.column_stack((x, y, d, z)), columns=x_cols + ["y", "d"] + z_cols) return data diff --git a/doubleml/plm/tests/test_pliv.py b/doubleml/plm/tests/test_pliv.py index ff2aa4938..7ee248f25 100644 --- a/doubleml/plm/tests/test_pliv.py +++ b/doubleml/plm/tests/test_pliv.py @@ -12,29 +12,27 @@ from ._utils_pliv_manual import boot_pliv, fit_pliv -@pytest.fixture(scope='module', - params=[RandomForestRegressor(max_depth=2, n_estimators=10), - LinearRegression(), - Lasso(alpha=0.1)]) +@pytest.fixture( + scope="module", params=[RandomForestRegressor(max_depth=2, n_estimators=10), LinearRegression(), Lasso(alpha=0.1)] +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out', 'IV-type']) +@pytest.fixture(scope="module", params=["partialling out", "IV-type"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_fixture(generate_data_iv, learner, score): - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 # collect data data = generate_data_iv - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for l, m, r & g ml_l = clone(learner) @@ -43,71 +41,75 @@ def dml_pliv_fixture(generate_data_iv, learner, score): ml_g = clone(learner) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'Z1') - if score == 'partialling out': - dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_l, ml_m, ml_r, - n_folds=n_folds, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, "Z1") + if score == "partialling out": + dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, ml_l, ml_m, ml_r, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_l, ml_m, ml_r, ml_g, - n_folds=n_folds, - score=score) + assert score == "IV-type" + dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, score=score) dml_pliv_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values - z = data['Z1'].values + d = data["d"].values + z = data["Z1"].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_pliv(y, x, d, z, - clone(learner), clone(learner), clone(learner), clone(learner), - all_smpls, score) + res_manual = fit_pliv(y, x, d, z, clone(learner), clone(learner), clone(learner), clone(learner), all_smpls, score) - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], - res_manual['all_g_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_r_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_pliv_coef(dml_pliv_fixture): - assert math.isclose(dml_pliv_fixture['coef'], - dml_pliv_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_fixture["coef"], dml_pliv_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_pliv_se(dml_pliv_fixture): - assert math.isclose(dml_pliv_fixture['se'], - dml_pliv_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_fixture["se"], dml_pliv_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_pliv_boot(dml_pliv_fixture): - for bootstrap in dml_pliv_fixture['boot_methods']: - assert np.allclose(dml_pliv_fixture['boot_t_stat' + bootstrap], - dml_pliv_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_fixture["boot_t_stat" + bootstrap], + dml_pliv_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_external_predictions.py b/doubleml/plm/tests/test_pliv_external_predictions.py index 2755d2002..bc8a1e8a4 100644 --- a/doubleml/plm/tests/test_pliv_external_predictions.py +++ b/doubleml/plm/tests/test_pliv_external_predictions.py @@ -33,9 +33,7 @@ def adapted_doubleml_fixture(score, n_rep, dim_z): else: ext_predictions = {"d": {}} - data = make_pliv_CHS2015( - n_obs=500, dim_x=20, alpha=0.5, dim_z=dim_z, return_type="DataFrame" - ) + data = make_pliv_CHS2015(n_obs=500, dim_x=20, alpha=0.5, dim_z=dim_z, return_type="DataFrame") np.random.seed(3141) @@ -74,9 +72,7 @@ def adapted_doubleml_fixture(score, n_rep, dim_z): ml_m_key = "ml_m_" + "Z" + str(instr + 1) ext_predictions["d"][ml_m_key] = dml_pliv.predictions[ml_m_key][:, :, 0] - dml_pliv_ext = DoubleMLPLIV( - ml_m=DMLDummyRegressor(), ml_l=DMLDummyRegressor(), ml_r=DMLDummyRegressor(), **kwargs - ) + dml_pliv_ext = DoubleMLPLIV(ml_m=DMLDummyRegressor(), ml_l=DMLDummyRegressor(), ml_r=DMLDummyRegressor(), **kwargs) np.random.seed(3141) dml_pliv_ext.fit(external_predictions=ext_predictions) diff --git a/doubleml/plm/tests/test_pliv_partial_x.py b/doubleml/plm/tests/test_pliv_partial_x.py index 338d42ef1..7fa922402 100644 --- a/doubleml/plm/tests/test_pliv_partial_x.py +++ b/doubleml/plm/tests/test_pliv_partial_x.py @@ -11,21 +11,19 @@ from ._utils_pliv_partial_x_manual import boot_pliv_partial_x, fit_pliv_partial_x -@pytest.fixture(scope='module', - params=[Lasso(alpha=0.1)]) +@pytest.fixture(scope="module", params=[Lasso(alpha=0.1)]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out']) +@pytest.fixture(scope="module", params=["partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner, score): - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 @@ -38,9 +36,7 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner, score): ml_r = clone(learner) np.random.seed(3141) - dml_pliv_obj = dml.DoubleMLPLIV._partialX(obj_dml_data, - ml_l, ml_m, ml_r, - n_folds=n_folds) + dml_pliv_obj = dml.DoubleMLPLIV._partialX(obj_dml_data, ml_l, ml_m, ml_r, n_folds=n_folds) dml_pliv_obj.fit(store_predictions=True) @@ -52,46 +48,57 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner, score): n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_pliv_partial_x(y, x, d, z, - clone(learner), clone(learner), clone(learner), - all_smpls, score) + res_manual = fit_pliv_partial_x(y, x, d, z, clone(learner), clone(learner), clone(learner), all_smpls, score) - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv_partial_x(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], - res_manual['all_r_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv_partial_x( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_r_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict def test_dml_pliv_coef(dml_pliv_partial_x_fixture): - assert math.isclose(dml_pliv_partial_x_fixture['coef'], - dml_pliv_partial_x_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_x_fixture["coef"], dml_pliv_partial_x_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_se(dml_pliv_partial_x_fixture): - assert math.isclose(dml_pliv_partial_x_fixture['se'], - dml_pliv_partial_x_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_partial_x_fixture["se"], dml_pliv_partial_x_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) def test_dml_pliv_boot(dml_pliv_partial_x_fixture): - for bootstrap in dml_pliv_partial_x_fixture['boot_methods']: - assert np.allclose(dml_pliv_partial_x_fixture['boot_t_stat' + bootstrap], - dml_pliv_partial_x_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_partial_x_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_partial_x_fixture["boot_t_stat" + bootstrap], + dml_pliv_partial_x_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_partial_x_tune.py b/doubleml/plm/tests/test_pliv_partial_x_tune.py index fa9d6475b..2526c9e45 100644 --- a/doubleml/plm/tests/test_pliv_partial_x_tune.py +++ b/doubleml/plm/tests/test_pliv_partial_x_tune.py @@ -12,54 +12,46 @@ from ._utils_pliv_partial_x_manual import boot_pliv_partial_x, fit_pliv_partial_x, tune_nuisance_pliv_partial_x -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_l(request): return request.param -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_r(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out']) +@pytest.fixture(scope="module", params=["partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ == RandomForestRegressor: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ == ElasticNet - par_grid = {'l1_ratio': [.1, .5, .7, .9, .95, .99, 1], 'alpha': np.linspace(0.05, 1., 7)} + par_grid = {"l1_ratio": [0.1, 0.5, 0.7, 0.9, 0.95, 0.99, 1], "alpha": np.linspace(0.05, 1.0, 7)} return par_grid -@pytest.fixture(scope='module') -def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_l, learner_m, learner_r, score, - tune_on_folds): - par_grid = {'ml_l': get_par_grid(learner_l), - 'ml_m': get_par_grid(learner_m), - 'ml_r': get_par_grid(learner_r)} +@pytest.fixture(scope="module") +def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_l, learner_m, learner_r, score, tune_on_folds): + par_grid = {"ml_l": get_par_grid(learner_l), "ml_m": get_par_grid(learner_m), "ml_r": get_par_grid(learner_r)} n_folds_tune = 4 - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 @@ -72,9 +64,7 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_l, learner_m ml_r = clone(learner_r) np.random.seed(3141) - dml_pliv_obj = dml.DoubleMLPLIV._partialX(obj_dml_data, - ml_l, ml_m, ml_r, - n_folds=n_folds) + dml_pliv_obj = dml.DoubleMLPLIV._partialX(obj_dml_data, ml_l, ml_m, ml_r, n_folds=n_folds) # tune hyperparameters _ = dml_pliv_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune) @@ -91,68 +81,103 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_l, learner_m smpls = all_smpls[0] if tune_on_folds: - l_params, m_params, r_params = tune_nuisance_pliv_partial_x(y, x, d, z, - clone(learner_l), - clone(learner_m), - clone(learner_r), - smpls, n_folds_tune, - par_grid['ml_l'], - par_grid['ml_m'], - par_grid['ml_r']) + l_params, m_params, r_params = tune_nuisance_pliv_partial_x( + y, + x, + d, + z, + clone(learner_l), + clone(learner_m), + clone(learner_r), + smpls, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_r"], + ) else: xx = [(np.arange(len(y)), np.array([]))] - l_params, m_params, r_params = tune_nuisance_pliv_partial_x(y, x, d, z, - clone(learner_l), - clone(learner_m), - clone(learner_r), - xx, n_folds_tune, - par_grid['ml_l'], - par_grid['ml_m'], - par_grid['ml_r']) + l_params, m_params, r_params = tune_nuisance_pliv_partial_x( + y, + x, + d, + z, + clone(learner_l), + clone(learner_m), + clone(learner_r), + xx, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_r"], + ) l_params = l_params * n_folds m_params = [xx * n_folds for xx in m_params] r_params = r_params * n_folds - res_manual = fit_pliv_partial_x(y, x, d, z, - clone(learner_l), clone(learner_m), clone(learner_r), - all_smpls, score, - l_params=l_params, m_params=m_params, r_params=r_params) - - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_pliv_partial_x( + y, + x, + d, + z, + clone(learner_l), + clone(learner_m), + clone(learner_r), + all_smpls, + score, + l_params=l_params, + m_params=m_params, + r_params=r_params, + ) + + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv_partial_x(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], - res_manual['all_r_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv_partial_x( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_r_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict def test_dml_pliv_coef(dml_pliv_partial_x_fixture): - assert math.isclose(dml_pliv_partial_x_fixture['coef'], - dml_pliv_partial_x_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_x_fixture["coef"], dml_pliv_partial_x_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_se(dml_pliv_partial_x_fixture): - assert math.isclose(dml_pliv_partial_x_fixture['se'], - dml_pliv_partial_x_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_partial_x_fixture["se"], dml_pliv_partial_x_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) def test_dml_pliv_boot(dml_pliv_partial_x_fixture): - for bootstrap in dml_pliv_partial_x_fixture['boot_methods']: - assert np.allclose(dml_pliv_partial_x_fixture['boot_t_stat' + bootstrap], - dml_pliv_partial_x_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_partial_x_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_partial_x_fixture["boot_t_stat" + bootstrap], + dml_pliv_partial_x_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_partial_xz.py b/doubleml/plm/tests/test_pliv_partial_xz.py index cb9722c11..8eb357e44 100644 --- a/doubleml/plm/tests/test_pliv_partial_xz.py +++ b/doubleml/plm/tests/test_pliv_partial_xz.py @@ -11,21 +11,19 @@ from ._utils_pliv_partial_xz_manual import boot_pliv_partial_xz, fit_pliv_partial_xz -@pytest.fixture(scope='module', - params=[Lasso(alpha=0.1)]) +@pytest.fixture(scope="module", params=[Lasso(alpha=0.1)]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out']) +@pytest.fixture(scope="module", params=["partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner, score): - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 @@ -38,9 +36,7 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner, score): ml_r = clone(learner) np.random.seed(3141) - dml_pliv_obj = dml.DoubleMLPLIV._partialXZ(obj_dml_data, - ml_l, ml_m, ml_r, - n_folds=n_folds) + dml_pliv_obj = dml.DoubleMLPLIV._partialXZ(obj_dml_data, ml_l, ml_m, ml_r, n_folds=n_folds) dml_pliv_obj.fit() @@ -52,45 +48,58 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner, score): n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_pliv_partial_xz(y, x, d, z, - clone(learner), clone(learner), clone(learner), - all_smpls, score) + res_manual = fit_pliv_partial_xz(y, x, d, z, clone(learner), clone(learner), clone(learner), all_smpls, score) - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv_partial_xz(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], - res_manual['all_r_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv_partial_xz( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_r_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict def test_dml_pliv_coef(dml_pliv_partial_xz_fixture): - assert math.isclose(dml_pliv_partial_xz_fixture['coef'], - dml_pliv_partial_xz_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_xz_fixture["coef"], dml_pliv_partial_xz_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_se(dml_pliv_partial_xz_fixture): - assert math.isclose(dml_pliv_partial_xz_fixture['se'], - dml_pliv_partial_xz_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_xz_fixture["se"], dml_pliv_partial_xz_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_boot(dml_pliv_partial_xz_fixture): - for bootstrap in dml_pliv_partial_xz_fixture['boot_methods']: - assert np.allclose(dml_pliv_partial_xz_fixture['boot_t_stat' + bootstrap], - dml_pliv_partial_xz_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_partial_xz_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_partial_xz_fixture["boot_t_stat" + bootstrap], + dml_pliv_partial_xz_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_partial_xz_tune.py b/doubleml/plm/tests/test_pliv_partial_xz_tune.py index 758cbde7b..48f845613 100644 --- a/doubleml/plm/tests/test_pliv_partial_xz_tune.py +++ b/doubleml/plm/tests/test_pliv_partial_xz_tune.py @@ -12,54 +12,46 @@ from ._utils_pliv_partial_xz_manual import boot_pliv_partial_xz, fit_pliv_partial_xz, tune_nuisance_pliv_partial_xz -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_l(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestRegressor()]) +@pytest.fixture(scope="module", params=[RandomForestRegressor()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[RandomForestRegressor()]) +@pytest.fixture(scope="module", params=[RandomForestRegressor()]) def learner_r(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out']) +@pytest.fixture(scope="module", params=["partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ == RandomForestRegressor: - par_grid = {'n_estimators': [5, 10, 20]} + par_grid = {"n_estimators": [5, 10, 20]} else: assert learner.__class__ == ElasticNet - par_grid = {'l1_ratio': [.1, .5, .7, .9, .95, .99, 1], 'alpha': np.linspace(0.05, 1., 7)} + par_grid = {"l1_ratio": [0.1, 0.5, 0.7, 0.9, 0.95, 0.99, 1], "alpha": np.linspace(0.05, 1.0, 7)} return par_grid -@pytest.fixture(scope='module') -def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_l, learner_m, learner_r, score, - tune_on_folds): - par_grid = {'ml_l': get_par_grid(learner_l), - 'ml_m': get_par_grid(learner_m), - 'ml_r': get_par_grid(learner_r)} +@pytest.fixture(scope="module") +def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_l, learner_m, learner_r, score, tune_on_folds): + par_grid = {"ml_l": get_par_grid(learner_l), "ml_m": get_par_grid(learner_m), "ml_r": get_par_grid(learner_r)} n_folds_tune = 4 - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 @@ -72,9 +64,7 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_l, learner ml_r = clone(learner_r) np.random.seed(3141) - dml_pliv_obj = dml.DoubleMLPLIV._partialXZ(obj_dml_data, - ml_l, ml_m, ml_r, - n_folds=n_folds) + dml_pliv_obj = dml.DoubleMLPLIV._partialXZ(obj_dml_data, ml_l, ml_m, ml_r, n_folds=n_folds) # tune hyperparameters _ = dml_pliv_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune) @@ -91,68 +81,105 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_l, learner smpls = all_smpls[0] if tune_on_folds: - l_params, m_params, r_params = tune_nuisance_pliv_partial_xz(y, x, d, z, - clone(learner_l), - clone(learner_m), - clone(learner_r), - smpls, n_folds_tune, - par_grid['ml_l'], - par_grid['ml_m'], - par_grid['ml_r']) + l_params, m_params, r_params = tune_nuisance_pliv_partial_xz( + y, + x, + d, + z, + clone(learner_l), + clone(learner_m), + clone(learner_r), + smpls, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_r"], + ) else: xx = [(np.arange(len(y)), np.arange(len(y)))] - l_params, m_params, r_params = tune_nuisance_pliv_partial_xz(y, x, d, z, - clone(learner_l), - clone(learner_m), - clone(learner_r), - xx, n_folds_tune, - par_grid['ml_l'], - par_grid['ml_m'], - par_grid['ml_r']) + l_params, m_params, r_params = tune_nuisance_pliv_partial_xz( + y, + x, + d, + z, + clone(learner_l), + clone(learner_m), + clone(learner_r), + xx, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_r"], + ) l_params = l_params * n_folds m_params = m_params * n_folds r_params = r_params * n_folds - res_manual = fit_pliv_partial_xz(y, x, d, z, - clone(learner_l), clone(learner_m), clone(learner_r), - all_smpls, score, - l_params=l_params, m_params=m_params, r_params=r_params) - - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_pliv_partial_xz( + y, + x, + d, + z, + clone(learner_l), + clone(learner_m), + clone(learner_r), + all_smpls, + score, + l_params=l_params, + m_params=m_params, + r_params=r_params, + ) + + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv_partial_xz(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], - res_manual['all_r_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv_partial_xz( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_r_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict def test_dml_pliv_coef(dml_pliv_partial_xz_fixture): - assert math.isclose(dml_pliv_partial_xz_fixture['coef'], - dml_pliv_partial_xz_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_xz_fixture["coef"], dml_pliv_partial_xz_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_se(dml_pliv_partial_xz_fixture): - assert math.isclose(dml_pliv_partial_xz_fixture['se'], - dml_pliv_partial_xz_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_xz_fixture["se"], dml_pliv_partial_xz_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_boot(dml_pliv_partial_xz_fixture): - for bootstrap in dml_pliv_partial_xz_fixture['boot_methods']: - assert np.allclose(dml_pliv_partial_xz_fixture['boot_t_stat' + bootstrap], - dml_pliv_partial_xz_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_partial_xz_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_partial_xz_fixture["boot_t_stat" + bootstrap], + dml_pliv_partial_xz_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_partial_z.py b/doubleml/plm/tests/test_pliv_partial_z.py index 3fb7a647b..05157088b 100644 --- a/doubleml/plm/tests/test_pliv_partial_z.py +++ b/doubleml/plm/tests/test_pliv_partial_z.py @@ -11,86 +11,83 @@ from ._utils_pliv_partial_z_manual import boot_pliv_partial_z, fit_pliv_partial_z -@pytest.fixture(scope='module', - params=[Lasso(alpha=0.1)]) +@pytest.fixture(scope="module", params=[Lasso(alpha=0.1)]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out']) +@pytest.fixture(scope="module", params=["partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_partial_z_fixture(generate_data_pliv_partialZ, learner, score): - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 # collect data data = generate_data_pliv_partialZ - x_cols = data.columns[data.columns.str.startswith('X')].tolist() - z_cols = data.columns[data.columns.str.startswith('Z')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() + z_cols = data.columns[data.columns.str.startswith("Z")].tolist() # Set machine learning methods for r ml_r = clone(learner) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, z_cols) - dml_pliv_obj = dml.DoubleMLPLIV._partialZ(obj_dml_data, - ml_r, - n_folds=n_folds) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, z_cols) + dml_pliv_obj = dml.DoubleMLPLIV._partialZ(obj_dml_data, ml_r, n_folds=n_folds) dml_pliv_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values + d = data["d"].values z = data.loc[:, z_cols].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_pliv_partial_z(y, x, d, z, - clone(learner), - all_smpls, score) + res_manual = fit_pliv_partial_z(y, x, d, z, clone(learner), all_smpls, score) - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv_partial_z(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_r_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv_partial_z( + y, d, z, res_manual["thetas"], res_manual["ses"], res_manual["all_r_hat"], all_smpls, score, bootstrap, n_rep_boot + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict def test_dml_pliv_coef(dml_pliv_partial_z_fixture): - assert math.isclose(dml_pliv_partial_z_fixture['coef'], - dml_pliv_partial_z_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_z_fixture["coef"], dml_pliv_partial_z_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_se(dml_pliv_partial_z_fixture): - assert math.isclose(dml_pliv_partial_z_fixture['se'], - dml_pliv_partial_z_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_partial_z_fixture["se"], dml_pliv_partial_z_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) def test_dml_pliv_boot(dml_pliv_partial_z_fixture): - for bootstrap in dml_pliv_partial_z_fixture['boot_methods']: - assert np.allclose(dml_pliv_partial_z_fixture['boot_t_stat' + bootstrap], - dml_pliv_partial_z_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_partial_z_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_partial_z_fixture["boot_t_stat" + bootstrap], + dml_pliv_partial_z_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_partial_z_tune.py b/doubleml/plm/tests/test_pliv_partial_z_tune.py index bdc322d19..1763d9c29 100644 --- a/doubleml/plm/tests/test_pliv_partial_z_tune.py +++ b/doubleml/plm/tests/test_pliv_partial_z_tune.py @@ -11,52 +11,47 @@ from ._utils_pliv_partial_z_manual import boot_pliv_partial_z, fit_pliv_partial_z, tune_nuisance_pliv_partial_z -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_r(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out']) +@pytest.fixture(scope="module", params=["partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): assert learner.__class__ == ElasticNet - par_grid = {'l1_ratio': [.1, .5, .7, .9, .95, .99, 1], 'alpha': np.linspace(0.05, 1., 7)} + par_grid = {"l1_ratio": [0.1, 0.5, 0.7, 0.9, 0.95, 0.99, 1], "alpha": np.linspace(0.05, 1.0, 7)} return par_grid -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_partial_z_fixture(generate_data_pliv_partialZ, learner_r, score, tune_on_folds): - par_grid = {'ml_r': get_par_grid(learner_r)} + par_grid = {"ml_r": get_par_grid(learner_r)} n_folds_tune = 4 - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 # collect data data = generate_data_pliv_partialZ - x_cols = data.columns[data.columns.str.startswith('X')].tolist() - z_cols = data.columns[data.columns.str.startswith('Z')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() + z_cols = data.columns[data.columns.str.startswith("Z")].tolist() # Set machine learning methods for r ml_r = clone(learner_r) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, z_cols) - dml_pliv_obj = dml.DoubleMLPLIV._partialZ(obj_dml_data, - ml_r, - n_folds=n_folds) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, z_cols) + dml_pliv_obj = dml.DoubleMLPLIV._partialZ(obj_dml_data, ml_r, n_folds=n_folds) # tune hyperparameters _ = dml_pliv_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune) @@ -64,66 +59,60 @@ def dml_pliv_partial_z_fixture(generate_data_pliv_partialZ, learner_r, score, tu dml_pliv_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values + d = data["d"].values z = data.loc[:, z_cols].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) smpls = all_smpls[0] if tune_on_folds: - r_params = tune_nuisance_pliv_partial_z(y, x, d, z, - clone(learner_r), - smpls, n_folds_tune, - par_grid['ml_r']) + r_params = tune_nuisance_pliv_partial_z(y, x, d, z, clone(learner_r), smpls, n_folds_tune, par_grid["ml_r"]) else: xx = [(np.arange(len(y)), np.array([]))] - r_params = tune_nuisance_pliv_partial_z(y, x, d, z, - clone(learner_r), - xx, n_folds_tune, - par_grid['ml_r']) + r_params = tune_nuisance_pliv_partial_z(y, x, d, z, clone(learner_r), xx, n_folds_tune, par_grid["ml_r"]) r_params = r_params * n_folds - res_manual = fit_pliv_partial_z(y, x, d, z, - clone(learner_r), - all_smpls, score, - r_params=r_params) + res_manual = fit_pliv_partial_z(y, x, d, z, clone(learner_r), all_smpls, score, r_params=r_params) - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv_partial_z(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_r_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv_partial_z( + y, d, z, res_manual["thetas"], res_manual["ses"], res_manual["all_r_hat"], all_smpls, score, bootstrap, n_rep_boot + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict def test_dml_pliv_coef(dml_pliv_partial_z_fixture): - assert math.isclose(dml_pliv_partial_z_fixture['coef'], - dml_pliv_partial_z_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_partial_z_fixture["coef"], dml_pliv_partial_z_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) def test_dml_pliv_se(dml_pliv_partial_z_fixture): - assert math.isclose(dml_pliv_partial_z_fixture['se'], - dml_pliv_partial_z_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_partial_z_fixture["se"], dml_pliv_partial_z_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) def test_dml_pliv_boot(dml_pliv_partial_z_fixture): - for bootstrap in dml_pliv_partial_z_fixture['boot_methods']: - assert np.allclose(dml_pliv_partial_z_fixture['boot_t_stat' + bootstrap], - dml_pliv_partial_z_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_partial_z_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_partial_z_fixture["boot_t_stat" + bootstrap], + dml_pliv_partial_z_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_pliv_tune.py b/doubleml/plm/tests/test_pliv_tune.py index 80240bd3b..9da095836 100644 --- a/doubleml/plm/tests/test_pliv_tune.py +++ b/doubleml/plm/tests/test_pliv_tune.py @@ -10,164 +10,202 @@ from ._utils_pliv_manual import boot_pliv, fit_pliv, tune_nuisance_pliv -@pytest.fixture(scope='module', - params=[Lasso(), - ElasticNet()]) +@pytest.fixture(scope="module", params=[Lasso(), ElasticNet()]) def learner_l(request): return request.param -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_r(request): return request.param -@pytest.fixture(scope='module', - params=[ElasticNet()]) +@pytest.fixture(scope="module", params=[ElasticNet()]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out', 'IV-type']) +@pytest.fixture(scope="module", params=["partialling out", "IV-type"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ == Lasso: - par_grid = {'alpha': np.linspace(0.05, .95, 7)} + par_grid = {"alpha": np.linspace(0.05, 0.95, 7)} else: assert learner.__class__ == ElasticNet - par_grid = {'l1_ratio': [.1, .5, .7, .9, .95, .99, 1], 'alpha': np.linspace(0.05, 1., 7)} + par_grid = {"l1_ratio": [0.1, 0.5, 0.7, 0.9, 0.95, 0.99, 1], "alpha": np.linspace(0.05, 1.0, 7)} return par_grid -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_g, score, tune_on_folds): - par_grid = {'ml_l': get_par_grid(learner_l), - 'ml_m': get_par_grid(learner_m), - 'ml_r': get_par_grid(learner_r), - 'ml_g': get_par_grid(learner_g)} + par_grid = { + "ml_l": get_par_grid(learner_l), + "ml_m": get_par_grid(learner_m), + "ml_r": get_par_grid(learner_r), + "ml_g": get_par_grid(learner_g), + } n_folds_tune = 4 - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 503 # collect data data = generate_data_iv - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for l, m, r & g ml_l = _clone(learner_l) ml_m = _clone(learner_m) ml_r = _clone(learner_r) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner_g) else: ml_g = None np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'Z1') - dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_l, ml_m, ml_r, ml_g, - n_folds=n_folds, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols, "Z1") + dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, score=score) # tune hyperparameters - tune_res = dml_pliv_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=False) + tune_res = dml_pliv_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=False) assert isinstance(tune_res, dml.DoubleMLPLIV) dml_pliv_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values - z = data['Z1'].values + d = data["d"].values + z = data["Z1"].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) smpls = all_smpls[0] - tune_g = (score == 'IV-type') | callable(score) + tune_g = (score == "IV-type") | callable(score) if tune_on_folds: l_params, m_params, r_params, g_params = tune_nuisance_pliv( - y, x, d, z, - _clone(learner_l), _clone(learner_m), _clone(learner_r), _clone(learner_g), - smpls, n_folds_tune, - par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r'], par_grid['ml_g'], - tune_g) + y, + x, + d, + z, + _clone(learner_l), + _clone(learner_m), + _clone(learner_r), + _clone(learner_g), + smpls, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_r"], + par_grid["ml_g"], + tune_g, + ) else: xx = [(np.arange(len(y)), np.array([]))] l_params, m_params, r_params, g_params = tune_nuisance_pliv( - y, x, d, z, - _clone(learner_l), _clone(learner_m), _clone(learner_r), _clone(learner_g), - xx, n_folds_tune, - par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r'], par_grid['ml_g'], - tune_g) + y, + x, + d, + z, + _clone(learner_l), + _clone(learner_m), + _clone(learner_r), + _clone(learner_g), + xx, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_r"], + par_grid["ml_g"], + tune_g, + ) l_params = l_params * n_folds m_params = m_params * n_folds r_params = r_params * n_folds g_params = g_params * n_folds - res_manual = fit_pliv(y, x, d, z, _clone(learner_l), _clone(learner_m), _clone(learner_r), _clone(learner_g), - all_smpls, score, - l_params=l_params, m_params=m_params, r_params=r_params, g_params=g_params) - - res_dict = {'coef': dml_pliv_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_pliv_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_pliv( + y, + x, + d, + z, + _clone(learner_l), + _clone(learner_m), + _clone(learner_r), + _clone(learner_g), + all_smpls, + score, + l_params=l_params, + m_params=m_params, + r_params=r_params, + g_params=g_params, + ) + + res_dict = { + "coef": dml_pliv_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_pliv_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_pliv(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], - res_manual['all_r_hat'], res_manual['all_g_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_pliv( + y, + d, + z, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_r_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_pliv_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_pliv_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_pliv_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_pliv_coef(dml_pliv_fixture): - assert math.isclose(dml_pliv_fixture['coef'], - dml_pliv_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_fixture["coef"], dml_pliv_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_pliv_se(dml_pliv_fixture): - assert math.isclose(dml_pliv_fixture['se'], - dml_pliv_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_pliv_fixture["se"], dml_pliv_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_pliv_boot(dml_pliv_fixture): - for bootstrap in dml_pliv_fixture['boot_methods']: - assert np.allclose(dml_pliv_fixture['boot_t_stat' + bootstrap], - dml_pliv_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_pliv_fixture["boot_methods"]: + assert np.allclose( + dml_pliv_fixture["boot_t_stat" + bootstrap], + dml_pliv_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_plr.py b/doubleml/plm/tests/test_plr.py index ba1f132a2..79f21f849 100644 --- a/doubleml/plm/tests/test_plr.py +++ b/doubleml/plm/tests/test_plr.py @@ -14,29 +14,27 @@ from ._utils_plr_manual import boot_plr, fit_plr, fit_sensitivity_elements_plr, plr_dml2 -@pytest.fixture(scope='module', - params=[RandomForestRegressor(max_depth=2, n_estimators=10), - LinearRegression(), - Lasso(alpha=0.1)]) +@pytest.fixture( + scope="module", params=[RandomForestRegressor(max_depth=2, n_estimators=10), LinearRegression(), Lasso(alpha=0.1)] +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param @pytest.fixture(scope="module") def dml_plr_fixture(generate_data1, learner, score): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 502 # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for m & g ml_l = clone(learner) @@ -44,175 +42,170 @@ def dml_plr_fixture(generate_data1, learner, score): ml_g = clone(learner) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - if score == 'partialling out': - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) + if score == "partialling out": + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) dml_plr_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values + d = data["d"].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_plr(y, x, d, clone(learner), clone(learner), clone(learner), - all_smpls, score) + res_manual = fit_plr(y, x, d, clone(learner), clone(learner), clone(learner), all_smpls, score) np.random.seed(3141) # test with external nuisance predictions - if score == 'partialling out': - dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, - n_folds, - score=score) + if score == "partialling out": + dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) # synchronize the sample splitting dml_plr_obj_ext.set_sample_splitting(all_smpls=all_smpls) - if score == 'partialling out': - prediction_dict = {'d': {'ml_l': dml_plr_obj.predictions['ml_l'].reshape(-1, 1), - 'ml_m': dml_plr_obj.predictions['ml_m'].reshape(-1, 1)}} + if score == "partialling out": + prediction_dict = { + "d": { + "ml_l": dml_plr_obj.predictions["ml_l"].reshape(-1, 1), + "ml_m": dml_plr_obj.predictions["ml_m"].reshape(-1, 1), + } + } else: - assert score == 'IV-type' - prediction_dict = {'d': {'ml_l': dml_plr_obj.predictions['ml_l'].reshape(-1, 1), - 'ml_m': dml_plr_obj.predictions['ml_m'].reshape(-1, 1), - 'ml_g': dml_plr_obj.predictions['ml_g'].reshape(-1, 1)}} + assert score == "IV-type" + prediction_dict = { + "d": { + "ml_l": dml_plr_obj.predictions["ml_l"].reshape(-1, 1), + "ml_m": dml_plr_obj.predictions["ml_m"].reshape(-1, 1), + "ml_g": dml_plr_obj.predictions["ml_g"].reshape(-1, 1), + } + } dml_plr_obj_ext.fit(external_predictions=prediction_dict) - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'coef_ext': dml_plr_obj_ext.coef.item(), - 'se': dml_plr_obj.se.item(), - 'se_manual': res_manual['se'], - 'se_ext': dml_plr_obj_ext.se.item(), - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef_manual": res_manual["theta"], + "coef_ext": dml_plr_obj_ext.coef.item(), + "se": dml_plr_obj.se.item(), + "se_manual": res_manual["se"], + "se_ext": dml_plr_obj_ext.se.item(), + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_plr( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) np.random.seed(3141) dml_plr_obj_ext.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) - res_dict['boot_t_stat' + bootstrap + '_ext'] = dml_plr_obj_ext.boot_t_stat + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap + "_ext"] = dml_plr_obj_ext.boot_t_stat # sensitivity tests - res_dict['sensitivity_elements'] = dml_plr_obj.sensitivity_elements - res_dict['sensitivity_elements_manual'] = fit_sensitivity_elements_plr(y, d.reshape(-1, 1), - all_coef=dml_plr_obj.all_coef, - predictions=dml_plr_obj.predictions, - score=score, - n_rep=1) + res_dict["sensitivity_elements"] = dml_plr_obj.sensitivity_elements + res_dict["sensitivity_elements_manual"] = fit_sensitivity_elements_plr( + y, d.reshape(-1, 1), all_coef=dml_plr_obj.all_coef, predictions=dml_plr_obj.predictions, score=score, n_rep=1 + ) # check if sensitivity score with rho=0 gives equal asymptotic standard deviation dml_plr_obj.sensitivity_analysis(rho=0.0) - res_dict['sensitivity_ses'] = dml_plr_obj.sensitivity_params['se'] + res_dict["sensitivity_ses"] = dml_plr_obj.sensitivity_params["se"] return res_dict @pytest.mark.ci def test_dml_plr_coef(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['coef'], - dml_plr_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_plr_fixture['coef'], - dml_plr_fixture['coef_ext'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["coef"], dml_plr_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["coef"], dml_plr_fixture["coef_ext"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_se(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['se'], - dml_plr_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_plr_fixture['se'], - dml_plr_fixture['se_ext'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["se"], dml_plr_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["se"], dml_plr_fixture["se_ext"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_boot(dml_plr_fixture): - for bootstrap in dml_plr_fixture['boot_methods']: - assert np.allclose(dml_plr_fixture['boot_t_stat' + bootstrap], - dml_plr_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_fixture['boot_t_stat' + bootstrap], - dml_plr_fixture['boot_t_stat' + bootstrap + '_ext'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_fixture["boot_methods"]: + assert np.allclose( + dml_plr_fixture["boot_t_stat" + bootstrap], + dml_plr_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) + assert np.allclose( + dml_plr_fixture["boot_t_stat" + bootstrap], + dml_plr_fixture["boot_t_stat" + bootstrap + "_ext"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_plr_sensitivity(dml_plr_fixture): - sensitivity_element_names = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2'] + sensitivity_element_names = ["sigma2", "nu2", "psi_sigma2", "psi_nu2"] for sensitivity_element in sensitivity_element_names: - assert np.allclose(dml_plr_fixture['sensitivity_elements'][sensitivity_element], - dml_plr_fixture['sensitivity_elements_manual'][sensitivity_element]) + assert np.allclose( + dml_plr_fixture["sensitivity_elements"][sensitivity_element], + dml_plr_fixture["sensitivity_elements_manual"][sensitivity_element], + ) @pytest.mark.ci def test_dml_plr_sensitivity_rho0(dml_plr_fixture): - assert np.allclose(dml_plr_fixture['se'], - dml_plr_fixture['sensitivity_ses']['lower'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_fixture['se'], - dml_plr_fixture['sensitivity_ses']['upper'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_fixture["se"], dml_plr_fixture["sensitivity_ses"]["lower"], rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_fixture["se"], dml_plr_fixture["sensitivity_ses"]["upper"], rtol=1e-9, atol=1e-4) @pytest.fixture(scope="module") def dml_plr_ols_manual_fixture(generate_data1, score): learner = LinearRegression() - boot_methods = ['Bayes', 'normal', 'wild'] + boot_methods = ["Bayes", "normal", "wild"] n_folds = 2 n_rep_boot = 501 # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for m & g ml_l = clone(learner) ml_g = clone(learner) ml_m = clone(learner) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - if score == 'partialling out': - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) + if score == "partialling out": + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) n = data.shape[0] this_smpl = list() - xx = int(n/2) + xx = int(n / 2) this_smpl.append((np.arange(xx, n), np.arange(0, xx))) this_smpl.append((np.arange(0, xx), np.arange(xx, n))) smpls = [this_smpl] @@ -220,9 +213,9 @@ def dml_plr_ols_manual_fixture(generate_data1, score): dml_plr_obj.fit() - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values + d = data["d"].values # add column of ones for intercept o = np.ones((n, 1)) @@ -232,7 +225,7 @@ def dml_plr_ols_manual_fixture(generate_data1, score): l_hat = [] l_hat_vec = np.full_like(y, np.nan) - for (train_index, test_index) in smpls: + for train_index, test_index in smpls: ols_est = scipy.linalg.lstsq(x[train_index], y[train_index])[0] preds = np.dot(x[test_index], ols_est) l_hat.append(preds) @@ -240,68 +233,67 @@ def dml_plr_ols_manual_fixture(generate_data1, score): m_hat = [] m_hat_vec = np.full_like(d, np.nan) - for (train_index, test_index) in smpls: + for train_index, test_index in smpls: ols_est = scipy.linalg.lstsq(x[train_index], d[train_index])[0] preds = np.dot(x[test_index], ols_est) m_hat.append(preds) m_hat_vec[test_index] = preds g_hat = [] - if score == 'IV-type': + if score == "IV-type": theta_initial = scipy.linalg.lstsq((d - m_hat_vec).reshape(-1, 1), y - l_hat_vec)[0] - for (train_index, test_index) in smpls: - ols_est = scipy.linalg.lstsq(x[train_index], - y[train_index] - d[train_index] * theta_initial)[0] + for train_index, test_index in smpls: + ols_est = scipy.linalg.lstsq(x[train_index], y[train_index] - d[train_index] * theta_initial)[0] g_hat.append(np.dot(x[test_index], ols_est)) - res_manual, se_manual = plr_dml2(y, x, d, - l_hat, m_hat, g_hat, - smpls, score) + res_manual, se_manual = plr_dml2(y, x, d, l_hat, m_hat, g_hat, smpls, score) - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef_manual': res_manual.item(), - 'se': dml_plr_obj.se.item(), - 'se_manual': se_manual.item(), - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef_manual": res_manual.item(), + "se": dml_plr_obj.se.item(), + "se_manual": se_manual.item(), + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_plr(y, d, [res_manual], [se_manual], - [l_hat], [m_hat], [g_hat], - [smpls], score, bootstrap, n_rep_boot) + boot_t_stat = boot_plr( + y, d, [res_manual], [se_manual], [l_hat], [m_hat], [g_hat], [smpls], score, bootstrap, n_rep_boot + ) np.random.seed(3141) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_plr_ols_manual_coef(dml_plr_ols_manual_fixture): - assert math.isclose(dml_plr_ols_manual_fixture['coef'], - dml_plr_ols_manual_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_ols_manual_fixture["coef"], dml_plr_ols_manual_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_plr_ols_manual_se(dml_plr_ols_manual_fixture): - assert math.isclose(dml_plr_ols_manual_fixture['se'], - dml_plr_ols_manual_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_ols_manual_fixture["se"], dml_plr_ols_manual_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_ols_manual_boot(dml_plr_ols_manual_fixture): - for bootstrap in dml_plr_ols_manual_fixture['boot_methods']: - assert np.allclose(dml_plr_ols_manual_fixture['boot_t_stat' + bootstrap], - dml_plr_ols_manual_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_ols_manual_fixture["boot_methods"]: + assert np.allclose( + dml_plr_ols_manual_fixture["boot_t_stat" + bootstrap], + dml_plr_ols_manual_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) -@pytest.fixture(scope='module', - params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) +@pytest.fixture(scope="module", params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) def cov_type(request): return request.param @@ -317,10 +309,7 @@ def test_dml_plr_cate_gate(score, cov_type): ml_g = LinearRegression() ml_m = LinearRegression() - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, ml_l, - n_folds=2, - score=score) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_g, ml_m, ml_l, n_folds=2, score=score) dml_plr_obj.fit() random_basis = pd.DataFrame(np.random.normal(0, 1, size=(n, 5))) cate = dml_plr_obj.cate(random_basis, cov_type=cov_type) @@ -329,10 +318,9 @@ def test_dml_plr_cate_gate(score, cov_type): assert cate.blp_model.cov_type == cov_type groups_1 = pd.DataFrame( - np.column_stack([obj_dml_data.data['X1'] <= 0, - obj_dml_data.data['X1'] > 0.2]), - columns=['Group 1', 'Group 2']) - msg = ('At least one group effect is estimated with less than 6 observations.') + np.column_stack([obj_dml_data.data["X1"] <= 0, obj_dml_data.data["X1"] > 0.2]), columns=["Group 1", "Group 2"] + ) + msg = "At least one group effect is estimated with less than 6 observations." with pytest.warns(UserWarning, match=msg): gate_1 = dml_plr_obj.gate(groups_1, cov_type=cov_type) assert isinstance(gate_1, dml.utils.blp.DoubleMLBLP) @@ -342,7 +330,7 @@ def test_dml_plr_cate_gate(score, cov_type): np.random.seed(42) groups_2 = pd.DataFrame(np.random.choice(["1", "2"], n)) - msg = ('At least one group effect is estimated with less than 6 observations.') + msg = "At least one group effect is estimated with less than 6 observations." with pytest.warns(UserWarning, match=msg): gate_2 = dml_plr_obj.gate(groups_2, cov_type=cov_type) assert isinstance(gate_2, dml.utils.blp.DoubleMLBLP) diff --git a/doubleml/plm/tests/test_plr_classifier.py b/doubleml/plm/tests/test_plr_classifier.py index e39bb46b1..6b331346d 100644 --- a/doubleml/plm/tests/test_plr_classifier.py +++ b/doubleml/plm/tests/test_plr_classifier.py @@ -14,39 +14,32 @@ bonus_data = fetch_bonus() -@pytest.fixture(scope='module', - params=[Lasso(), - RandomForestClassifier(max_depth=2, n_estimators=10), - LogisticRegression()]) +@pytest.fixture(scope="module", params=[Lasso(), RandomForestClassifier(max_depth=2, n_estimators=10), LogisticRegression()]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param @pytest.fixture(scope="module") def dml_plr_binary_classifier_fixture(learner, score): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 502 # Set machine learning methods for l, m & g ml_l = Lasso(alpha=0.3) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = Lasso() else: ml_g = None np.random.seed(3141) - dml_plr_obj = dml.DoubleMLPLR(bonus_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + dml_plr_obj = dml.DoubleMLPLR(bonus_data, ml_l, ml_m, ml_g, n_folds, score=score) dml_plr_obj.fit() @@ -57,46 +50,60 @@ def dml_plr_binary_classifier_fixture(learner, score): n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_plr(y, x, d, _clone(ml_l), _clone(ml_m), _clone(ml_g), - all_smpls, score) + res_manual = fit_plr(y, x, d, _clone(ml_l), _clone(ml_m), _clone(ml_g), all_smpls, score) - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_plr_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_plr_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_plr( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_plr_binary_classifier_coef(dml_plr_binary_classifier_fixture): - assert math.isclose(dml_plr_binary_classifier_fixture['coef'], - dml_plr_binary_classifier_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_binary_classifier_fixture["coef"], dml_plr_binary_classifier_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_plr_binary_classifier_se(dml_plr_binary_classifier_fixture): - assert math.isclose(dml_plr_binary_classifier_fixture['se'], - dml_plr_binary_classifier_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_binary_classifier_fixture["se"], dml_plr_binary_classifier_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_plr_binary_classifier_boot(dml_plr_binary_classifier_fixture): - for bootstrap in dml_plr_binary_classifier_fixture['boot_methods']: - assert np.allclose(dml_plr_binary_classifier_fixture['boot_t_stat' + bootstrap], - dml_plr_binary_classifier_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_binary_classifier_fixture["boot_methods"]: + assert np.allclose( + dml_plr_binary_classifier_fixture["boot_t_stat" + bootstrap], + dml_plr_binary_classifier_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_plr_multi_treat.py b/doubleml/plm/tests/test_plr_multi_treat.py index 38e1713b5..0b4bea01c 100644 --- a/doubleml/plm/tests/test_plr_multi_treat.py +++ b/doubleml/plm/tests/test_plr_multi_treat.py @@ -9,35 +9,29 @@ from ._utils_plr_manual import boot_plr_multitreat, fit_plr_multitreat, fit_sensitivity_elements_plr -@pytest.fixture(scope='module', - params=range(2)) +@pytest.fixture(scope="module", params=range(2)) def idx(request): return request.param -@pytest.fixture(scope='module', - params=[Lasso(alpha=0.1), - RandomForestRegressor(max_depth=2, n_estimators=10)]) +@pytest.fixture(scope="module", params=[Lasso(alpha=0.1), RandomForestRegressor(max_depth=2, n_estimators=10)]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module') -def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, idx, learner, - score, n_rep): - boot_methods = ['normal'] +@pytest.fixture(scope="module") +def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, idx, learner, score, n_rep): + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 483 @@ -47,106 +41,110 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, else: assert idx == 1 data = generate_data_toeplitz - x_cols = data.columns[data.columns.str.startswith('X')].tolist() - d_cols = data.columns[data.columns.str.startswith('d')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() + d_cols = data.columns[data.columns.str.startswith("d")].tolist() n_coefs = len(d_cols) # Set machine learning methods for l, m & g ml_l = _clone(learner) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', d_cols, x_cols) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, n_rep, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", d_cols, x_cols) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, n_rep, score=score) dml_plr_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values d = data.loc[:, d_cols].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds, n_rep) - res_manual = fit_plr_multitreat(y, x, d, - _clone(learner), _clone(learner), _clone(learner), - all_smpls, score, n_rep=n_rep) + res_manual = fit_plr_multitreat(y, x, d, _clone(learner), _clone(learner), _clone(learner), all_smpls, score, n_rep=n_rep) - res_dict = {'coef': dml_plr_obj.coef, - 'coef_manual': res_manual['theta'], - 'se': dml_plr_obj.se, - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_plr_obj.coef, + "coef_manual": res_manual["theta"], + "se": dml_plr_obj.se, + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) boot_t_stat = boot_plr_multitreat( - y, d, - res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], - all_smpls, score, - bootstrap, n_rep_boot, n_rep) + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep, + ) np.random.seed(3141) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, n_coefs, n_rep) + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, n_coefs, n_rep) # sensitivity tests - res_dict['sensitivity_elements'] = dml_plr_obj.sensitivity_elements - res_dict['sensitivity_elements_manual'] = fit_sensitivity_elements_plr(y, d, - all_coef=dml_plr_obj.all_coef, - predictions=dml_plr_obj.predictions, - score=score, - n_rep=n_rep) + res_dict["sensitivity_elements"] = dml_plr_obj.sensitivity_elements + res_dict["sensitivity_elements_manual"] = fit_sensitivity_elements_plr( + y, d, all_coef=dml_plr_obj.all_coef, predictions=dml_plr_obj.predictions, score=score, n_rep=n_rep + ) # check if sensitivity score with rho=0 gives equal asymptotic standard deviation dml_plr_obj.sensitivity_analysis(rho=0.0) - res_dict['sensitivity_ses'] = dml_plr_obj.sensitivity_params['se'] + res_dict["sensitivity_ses"] = dml_plr_obj.sensitivity_params["se"] return res_dict @pytest.mark.ci def test_dml_plr_multitreat_coef(dml_plr_multitreat_fixture): - assert np.allclose(dml_plr_multitreat_fixture['coef'], - dml_plr_multitreat_fixture['coef_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_multitreat_fixture["coef"], dml_plr_multitreat_fixture["coef_manual"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_plr_multitreat_se(dml_plr_multitreat_fixture): - assert np.allclose(dml_plr_multitreat_fixture['se'], - dml_plr_multitreat_fixture['se_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_multitreat_fixture["se"], dml_plr_multitreat_fixture["se_manual"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_plr_multitreat_boot(dml_plr_multitreat_fixture): - for bootstrap in dml_plr_multitreat_fixture['boot_methods']: - assert np.allclose(dml_plr_multitreat_fixture['boot_t_stat' + bootstrap], - dml_plr_multitreat_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_multitreat_fixture["boot_methods"]: + assert np.allclose( + dml_plr_multitreat_fixture["boot_t_stat" + bootstrap], + dml_plr_multitreat_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_plr_multitreat_sensitivity(dml_plr_multitreat_fixture): - sensitivity_element_names = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2'] + sensitivity_element_names = ["sigma2", "nu2", "psi_sigma2", "psi_nu2"] for sensitivity_element in sensitivity_element_names: - assert np.allclose(dml_plr_multitreat_fixture['sensitivity_elements'][sensitivity_element], - dml_plr_multitreat_fixture['sensitivity_elements_manual'][sensitivity_element]) + assert np.allclose( + dml_plr_multitreat_fixture["sensitivity_elements"][sensitivity_element], + dml_plr_multitreat_fixture["sensitivity_elements_manual"][sensitivity_element], + ) @pytest.mark.ci def test_dml_plr_multitreat_sensitivity_rho0(dml_plr_multitreat_fixture): - assert np.allclose(dml_plr_multitreat_fixture['se'], - dml_plr_multitreat_fixture['sensitivity_ses']['lower'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_multitreat_fixture['se'], - dml_plr_multitreat_fixture['sensitivity_ses']['upper'], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_plr_multitreat_fixture["se"], dml_plr_multitreat_fixture["sensitivity_ses"]["lower"], rtol=1e-9, atol=1e-4 + ) + assert np.allclose( + dml_plr_multitreat_fixture["se"], dml_plr_multitreat_fixture["sensitivity_ses"]["upper"], rtol=1e-9, atol=1e-4 + ) diff --git a/doubleml/plm/tests/test_plr_reestimate_from_scores.py b/doubleml/plm/tests/test_plr_reestimate_from_scores.py index 83932f3bc..9f44d61be 100644 --- a/doubleml/plm/tests/test_plr_reestimate_from_scores.py +++ b/doubleml/plm/tests/test_plr_reestimate_from_scores.py @@ -9,20 +9,17 @@ from ...tests._utils import _clone -@pytest.fixture(scope='module', - params=[LinearRegression()]) +@pytest.fixture(scope="module", params=[LinearRegression()]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param @@ -33,53 +30,43 @@ def dml_plr_reestimate_fixture(generate_data1, learner, score, n_rep): # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for l, m & g ml_l = _clone(learner) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - n_rep, - score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, n_rep, score) dml_plr_obj.fit() np.random.seed(3141) - dml_plr_obj2 = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - n_rep, - score) + dml_plr_obj2 = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, n_rep, score) dml_plr_obj2.fit() dml_plr_obj2._coef[0] = np.nan dml_plr_obj2._se[0] = np.nan dml_plr_obj2._est_causal_pars_and_se() - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef2': dml_plr_obj2.coef.item(), - 'se': dml_plr_obj.se.item(), - 'se2': dml_plr_obj2.se.item()} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef2": dml_plr_obj2.coef.item(), + "se": dml_plr_obj.se.item(), + "se2": dml_plr_obj2.se.item(), + } return res_dict @pytest.mark.ci def test_dml_plr_coef(dml_plr_reestimate_fixture): - assert math.isclose(dml_plr_reestimate_fixture['coef'], - dml_plr_reestimate_fixture['coef2'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_reestimate_fixture["coef"], dml_plr_reestimate_fixture["coef2"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_se(dml_plr_reestimate_fixture): - assert math.isclose(dml_plr_reestimate_fixture['se'], - dml_plr_reestimate_fixture['se2'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_reestimate_fixture["se"], dml_plr_reestimate_fixture["se2"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/plm/tests/test_plr_rep_cross.py b/doubleml/plm/tests/test_plr_rep_cross.py index 8b17a923a..a5f237df1 100644 --- a/doubleml/plm/tests/test_plr_rep_cross.py +++ b/doubleml/plm/tests/test_plr_rep_cross.py @@ -11,132 +11,135 @@ from ._utils_plr_manual import boot_plr, fit_plr -@pytest.fixture(scope='module', - params=[RandomForestRegressor(max_depth=2, n_estimators=10), - LinearRegression()]) +@pytest.fixture(scope="module", params=[RandomForestRegressor(max_depth=2, n_estimators=10), LinearRegression()]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[3]) +@pytest.fixture(scope="module", params=[3]) def n_rep(request): return request.param @pytest.fixture(scope="module") def dml_plr_fixture(generate_data1, learner, score, n_rep): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 498 # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for l, m & g ml_l = _clone(learner) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - n_rep, - score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, n_rep, score) dml_plr_obj.fit() np.random.seed(3141) - y = data['y'].values + y = data["y"].values x = data.loc[:, x_cols].values - d = data['d'].values + d = data["d"].values n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds, n_rep) - res_manual = fit_plr(y, x, d, _clone(learner), _clone(learner), _clone(learner), - all_smpls, score, n_rep) + res_manual = fit_plr(y, x, d, _clone(learner), _clone(learner), _clone(learner), all_smpls, score, n_rep) np.random.seed(3141) # test with external nuisance predictions - if score == 'partialling out': - dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, - n_folds, - n_rep, - score=score) + if score == "partialling out": + dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds, n_rep, score=score) else: - assert score == 'IV-type' - dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - n_rep, - score=score) + assert score == "IV-type" + dml_plr_obj_ext = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, n_rep, score=score) # synchronize the sample splitting dml_plr_obj_ext.set_sample_splitting(all_smpls=all_smpls) - if score == 'partialling out': - prediction_dict = {'d': {'ml_l': dml_plr_obj.predictions['ml_l'].reshape(-1, n_rep), - 'ml_m': dml_plr_obj.predictions['ml_m'].reshape(-1, n_rep)}} + if score == "partialling out": + prediction_dict = { + "d": { + "ml_l": dml_plr_obj.predictions["ml_l"].reshape(-1, n_rep), + "ml_m": dml_plr_obj.predictions["ml_m"].reshape(-1, n_rep), + } + } else: - assert score == 'IV-type' - prediction_dict = {'d': {'ml_l': dml_plr_obj.predictions['ml_l'].reshape(-1, n_rep), - 'ml_m': dml_plr_obj.predictions['ml_m'].reshape(-1, n_rep), - 'ml_g': dml_plr_obj.predictions['ml_g'].reshape(-1, n_rep)}} + assert score == "IV-type" + prediction_dict = { + "d": { + "ml_l": dml_plr_obj.predictions["ml_l"].reshape(-1, n_rep), + "ml_m": dml_plr_obj.predictions["ml_m"].reshape(-1, n_rep), + "ml_g": dml_plr_obj.predictions["ml_g"].reshape(-1, n_rep), + } + } dml_plr_obj_ext.fit(external_predictions=prediction_dict) - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'coef_ext': dml_plr_obj_ext.coef, - 'se': dml_plr_obj.se.item(), - 'se_manual': res_manual['se'], - 'se_ext': dml_plr_obj_ext.se, - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef_manual": res_manual["theta"], + "coef_ext": dml_plr_obj_ext.coef, + "se": dml_plr_obj.se.item(), + "se_manual": res_manual["se"], + "se_ext": dml_plr_obj_ext.se, + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], - all_smpls, score, bootstrap, n_rep_boot, n_rep) + boot_t_stat = boot_plr( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + n_rep, + ) np.random.seed(3141) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, n_rep) + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, n_rep) return res_dict @pytest.mark.ci def test_dml_plr_coef(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['coef'], - dml_plr_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["coef"], dml_plr_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_se(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['se'], - dml_plr_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["se"], dml_plr_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_boot(dml_plr_fixture): - for bootstrap in dml_plr_fixture['boot_methods']: - assert np.allclose(dml_plr_fixture['boot_t_stat' + bootstrap], - dml_plr_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_fixture["boot_methods"]: + assert np.allclose( + dml_plr_fixture["boot_t_stat" + bootstrap], + dml_plr_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py b/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py index dccd3fc3a..08b6f5f16 100644 --- a/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py +++ b/doubleml/plm/tests/test_plr_set_ml_nuisance_pars.py @@ -9,15 +9,14 @@ from ...tests._utils import _clone -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param @pytest.fixture(scope="module") def dml_plr_fixture(generate_data1, score): - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 502 @@ -29,17 +28,14 @@ def dml_plr_fixture(generate_data1, score): # Set machine learning methods for l, m & g ml_l = _clone(learner) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d']) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"]) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) dml_plr_obj.fit() @@ -48,36 +44,35 @@ def dml_plr_fixture(generate_data1, score): # Set machine learning methods for l, m & g ml_l = _clone(learner) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None - dml_plr_obj_ext_set_par = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) - dml_plr_obj_ext_set_par.set_ml_nuisance_params('ml_l', 'd', {'alpha': alpha}) - dml_plr_obj_ext_set_par.set_ml_nuisance_params('ml_m', 'd', {'alpha': alpha}) - if score == 'IV-type': - dml_plr_obj_ext_set_par.set_ml_nuisance_params('ml_g', 'd', {'alpha': alpha}) + dml_plr_obj_ext_set_par = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) + dml_plr_obj_ext_set_par.set_ml_nuisance_params("ml_l", "d", {"alpha": alpha}) + dml_plr_obj_ext_set_par.set_ml_nuisance_params("ml_m", "d", {"alpha": alpha}) + if score == "IV-type": + dml_plr_obj_ext_set_par.set_ml_nuisance_params("ml_g", "d", {"alpha": alpha}) dml_plr_obj_ext_set_par.fit() - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef_manual': dml_plr_obj_ext_set_par.coef.item(), - 'se': dml_plr_obj.se.item(), - 'se_manual': dml_plr_obj_ext_set_par.se.item(), - 'boot_methods': boot_methods} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef_manual": dml_plr_obj_ext_set_par.coef.item(), + "se": dml_plr_obj.se.item(), + "se_manual": dml_plr_obj_ext_set_par.se.item(), + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(314122) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat np.random.seed(314122) dml_plr_obj_ext_set_par.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap + '_manual'] = dml_plr_obj_ext_set_par.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = dml_plr_obj_ext_set_par.boot_t_stat return res_dict @@ -85,23 +80,22 @@ def dml_plr_fixture(generate_data1, score): @pytest.mark.ci @pytest.mark.filterwarnings("ignore:Using the same") def test_dml_plr_coef(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['coef'], - dml_plr_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["coef"], dml_plr_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci @pytest.mark.filterwarnings("ignore:Using the same") def test_dml_plr_se(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['se'], - dml_plr_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["se"], dml_plr_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci @pytest.mark.filterwarnings("ignore:Using the same") def test_dml_plr_boot(dml_plr_fixture): - for bootstrap in dml_plr_fixture['boot_methods']: - assert np.allclose(dml_plr_fixture['boot_t_stat' + bootstrap], - dml_plr_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_fixture["boot_methods"]: + assert np.allclose( + dml_plr_fixture["boot_t_stat" + bootstrap], + dml_plr_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/plm/tests/test_plr_set_smpls_externally.py b/doubleml/plm/tests/test_plr_set_smpls_externally.py index 9476f126c..af81c9fc0 100644 --- a/doubleml/plm/tests/test_plr_set_smpls_externally.py +++ b/doubleml/plm/tests/test_plr_set_smpls_externally.py @@ -9,20 +9,17 @@ from ...tests._utils import _clone -@pytest.fixture(scope='module', - params=[LinearRegression()]) +@pytest.fixture(scope="module", params=[LinearRegression()]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param @@ -33,52 +30,43 @@ def dml_plr_smpls_fixture(generate_data1, learner, score, n_rep): # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for l, m & g ml_l = _clone(learner) ml_m = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - n_rep, - score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, n_rep, score) dml_plr_obj.fit() smpls = dml_plr_obj.smpls - dml_plr_obj2 = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - score=score, - draw_sample_splitting=False) + dml_plr_obj2 = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, score=score, draw_sample_splitting=False) dml_plr_obj2.set_sample_splitting(smpls) dml_plr_obj2.fit() - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef2': dml_plr_obj2.coef.item(), - 'se': dml_plr_obj.se.item(), - 'se2': dml_plr_obj2.se.item()} + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef2": dml_plr_obj2.coef.item(), + "se": dml_plr_obj.se.item(), + "se2": dml_plr_obj2.se.item(), + } return res_dict @pytest.mark.ci def test_dml_plr_coef(dml_plr_smpls_fixture): - assert math.isclose(dml_plr_smpls_fixture['coef'], - dml_plr_smpls_fixture['coef2'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_smpls_fixture["coef"], dml_plr_smpls_fixture["coef2"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_se(dml_plr_smpls_fixture): - assert math.isclose(dml_plr_smpls_fixture['se'], - dml_plr_smpls_fixture['se2'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_smpls_fixture["se"], dml_plr_smpls_fixture["se2"], rel_tol=1e-9, abs_tol=1e-4) diff --git a/doubleml/plm/tests/test_plr_tune.py b/doubleml/plm/tests/test_plr_tune.py index 6607f33eb..2e34a6d06 100644 --- a/doubleml/plm/tests/test_plr_tune.py +++ b/doubleml/plm/tests/test_plr_tune.py @@ -10,56 +10,46 @@ from ._utils_plr_manual import boot_plr, fit_plr, tune_nuisance_plr -@pytest.fixture(scope='module', - params=[Lasso(), - ElasticNet()]) +@pytest.fixture(scope="module", params=[Lasso(), ElasticNet()]) def learner_l(request): return request.param -@pytest.fixture(scope='module', - params=[Lasso(), - ElasticNet()]) +@pytest.fixture(scope="module", params=[Lasso(), ElasticNet()]) def learner_m(request): return request.param -@pytest.fixture(scope='module', - params=[Lasso(), - ElasticNet()]) +@pytest.fixture(scope="module", params=[Lasso(), ElasticNet()]) def learner_g(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out', 'IV-type']) +@pytest.fixture(scope="module", params=["partialling out", "IV-type"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def tune_on_folds(request): return request.param def get_par_grid(learner): if learner.__class__ == Lasso: - par_grid = {'alpha': np.linspace(0.05, .95, 7)} + par_grid = {"alpha": np.linspace(0.05, 0.95, 7)} else: assert learner.__class__ == ElasticNet - par_grid = {'l1_ratio': [.1, .5, .7, .9, .95, .99, 1], 'alpha': np.linspace(0.05, 1., 7)} + par_grid = {"l1_ratio": [0.1, 0.5, 0.7, 0.9, 0.95, 0.99, 1], "alpha": np.linspace(0.05, 1.0, 7)} return par_grid @pytest.fixture(scope="module") def dml_plr_fixture(generate_data2, learner_l, learner_m, learner_g, score, tune_on_folds): - par_grid = {'ml_l': get_par_grid(learner_l), - 'ml_m': get_par_grid(learner_m), - 'ml_g': get_par_grid(learner_g)} + par_grid = {"ml_l": get_par_grid(learner_l), "ml_m": get_par_grid(learner_m), "ml_g": get_par_grid(learner_g)} n_folds_tune = 4 - boot_methods = ['normal'] + boot_methods = ["normal"] n_folds = 2 n_rep_boot = 502 @@ -69,20 +59,16 @@ def dml_plr_fixture(generate_data2, learner_l, learner_m, learner_g, score, tune # Set machine learning methods for m & g ml_l = _clone(learner_l) ml_m = _clone(learner_m) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner_g) else: ml_g = None np.random.seed(3141) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) # tune hyperparameters - tune_res = dml_plr_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, - return_tune_res=True) + tune_res = dml_plr_obj.tune(par_grid, tune_on_folds=tune_on_folds, n_folds_tune=n_folds_tune, return_tune_res=True) assert isinstance(tune_res, list) # fit with tuned parameters @@ -96,63 +82,104 @@ def dml_plr_fixture(generate_data2, learner_l, learner_m, learner_g, score, tune all_smpls = draw_smpls(n_obs, n_folds) smpls = all_smpls[0] - tune_g = (score == 'IV-type') + tune_g = score == "IV-type" if tune_on_folds: - l_params, m_params, g_params = tune_nuisance_plr(y, x, d, - _clone(learner_l), _clone(learner_m), _clone(learner_g), - smpls, n_folds_tune, - par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_g'], tune_g) + l_params, m_params, g_params = tune_nuisance_plr( + y, + x, + d, + _clone(learner_l), + _clone(learner_m), + _clone(learner_g), + smpls, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_g"], + tune_g, + ) else: xx = [(np.arange(len(y)), np.array([]))] - l_params, m_params, g_params = tune_nuisance_plr(y, x, d, - _clone(learner_l), _clone(learner_m), _clone(learner_g), - xx, n_folds_tune, - par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_g'], tune_g) + l_params, m_params, g_params = tune_nuisance_plr( + y, + x, + d, + _clone(learner_l), + _clone(learner_m), + _clone(learner_g), + xx, + n_folds_tune, + par_grid["ml_l"], + par_grid["ml_m"], + par_grid["ml_g"], + tune_g, + ) l_params = l_params * n_folds g_params = g_params * n_folds m_params = m_params * n_folds - res_manual = fit_plr(y, x, d, _clone(learner_l), _clone(learner_m), _clone(learner_g), - all_smpls, score, - l_params=l_params, m_params=m_params, g_params=g_params) - - res_dict = {'coef': dml_plr_obj.coef.item(), - 'coef_manual': res_manual['theta'], - 'se': dml_plr_obj.se.item(), - 'se_manual': res_manual['se'], - 'boot_methods': boot_methods} + res_manual = fit_plr( + y, + x, + d, + _clone(learner_l), + _clone(learner_m), + _clone(learner_g), + all_smpls, + score, + l_params=l_params, + m_params=m_params, + g_params=g_params, + ) + + res_dict = { + "coef": dml_plr_obj.coef.item(), + "coef_manual": res_manual["theta"], + "se": dml_plr_obj.se.item(), + "se_manual": res_manual["se"], + "boot_methods": boot_methods, + } for bootstrap in boot_methods: np.random.seed(3141) - boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], - all_smpls, score, bootstrap, n_rep_boot) + boot_t_stat = boot_plr( + y, + d, + res_manual["thetas"], + res_manual["ses"], + res_manual["all_l_hat"], + res_manual["all_m_hat"], + res_manual["all_g_hat"], + all_smpls, + score, + bootstrap, + n_rep_boot, + ) np.random.seed(3141) dml_plr_obj.bootstrap(method=bootstrap, n_rep_boot=n_rep_boot) - res_dict['boot_t_stat' + bootstrap] = dml_plr_obj.boot_t_stat - res_dict['boot_t_stat' + bootstrap + '_manual'] = boot_t_stat.reshape(-1, 1, 1) + res_dict["boot_t_stat" + bootstrap] = dml_plr_obj.boot_t_stat + res_dict["boot_t_stat" + bootstrap + "_manual"] = boot_t_stat.reshape(-1, 1, 1) return res_dict @pytest.mark.ci def test_dml_plr_coef(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['coef'], - dml_plr_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["coef"], dml_plr_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_se(dml_plr_fixture): - assert math.isclose(dml_plr_fixture['se'], - dml_plr_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose(dml_plr_fixture["se"], dml_plr_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4) @pytest.mark.ci def test_dml_plr_boot(dml_plr_fixture): - for bootstrap in dml_plr_fixture['boot_methods']: - assert np.allclose(dml_plr_fixture['boot_t_stat' + bootstrap], - dml_plr_fixture['boot_t_stat' + bootstrap + '_manual'], - rtol=1e-9, atol=1e-4) + for bootstrap in dml_plr_fixture["boot_methods"]: + assert np.allclose( + dml_plr_fixture["boot_t_stat" + bootstrap], + dml_plr_fixture["boot_t_stat" + bootstrap + "_manual"], + rtol=1e-9, + atol=1e-4, + ) From 28c64e1bb7215f794a2f2f08c193e27d7525e225 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:26:40 +0100 Subject: [PATCH 10/28] run ruff on rdd --- doubleml/rdd/rdd.py | 9 ++++----- doubleml/rdd/tests/conftest.py | 10 ++++------ doubleml/rdd/tests/test_rdd_classifier.py | 9 ++++----- doubleml/rdd/tests/test_rdd_classifier_fuzzy.py | 2 +- .../rdd/tests/test_rdd_classifier_fuzzy_left.py | 2 +- .../rdd/tests/test_rdd_classifier_fuzzy_right.py | 2 +- doubleml/rdd/tests/test_rdd_classifier_sharp.py | 2 +- doubleml/rdd/tests/test_rdd_default_values.py | 5 ++--- doubleml/rdd/tests/test_rdd_exceptions.py | 14 +++++++------- doubleml/rdd/tests/test_rdd_fuzzy.py | 2 +- doubleml/rdd/tests/test_rdd_fuzzy_left.py | 2 +- doubleml/rdd/tests/test_rdd_fuzzy_right.py | 2 +- doubleml/rdd/tests/test_rdd_not_installed.py | 3 ++- doubleml/rdd/tests/test_rdd_return_types.py | 5 ++--- doubleml/rdd/tests/test_rdd_sharp.py | 2 +- 15 files changed, 33 insertions(+), 38 deletions(-) diff --git a/doubleml/rdd/rdd.py b/doubleml/rdd/rdd.py index 89aff94c5..c2e23f051 100644 --- a/doubleml/rdd/rdd.py +++ b/doubleml/rdd/rdd.py @@ -1,18 +1,17 @@ import warnings -import numpy as np -import pandas as pd from collections.abc import Callable +import numpy as np +import pandas as pd from scipy.stats import norm - from sklearn.base import clone from sklearn.utils.multiclass import type_of_target from doubleml import DoubleMLData from doubleml.double_ml import DoubleML -from doubleml.utils.resampling import DoubleMLResampling -from doubleml.utils._checks import _check_resampling_specification, _check_supports_sample_weights from doubleml.rdd._utils import _is_rdrobust_available +from doubleml.utils._checks import _check_resampling_specification, _check_supports_sample_weights +from doubleml.utils.resampling import DoubleMLResampling # validate optional rdrobust import rdrobust = _is_rdrobust_available() diff --git a/doubleml/rdd/tests/conftest.py b/doubleml/rdd/tests/conftest.py index 693c198cd..b850053e0 100644 --- a/doubleml/rdd/tests/conftest.py +++ b/doubleml/rdd/tests/conftest.py @@ -1,15 +1,13 @@ -import pytest - import numpy as np import pandas as pd +import pytest +from sklearn.dummy import DummyClassifier, DummyRegressor -from doubleml.rdd.datasets import make_simple_rdd_data from doubleml import DoubleMLData from doubleml.rdd import RDFlex - -from sklearn.dummy import DummyRegressor, DummyClassifier - from doubleml.rdd._utils import _is_rdrobust_available +from doubleml.rdd.datasets import make_simple_rdd_data + # validate optional rdrobust import rdrobust = _is_rdrobust_available() diff --git a/doubleml/rdd/tests/test_rdd_classifier.py b/doubleml/rdd/tests/test_rdd_classifier.py index 8054892f9..19b9c53cc 100644 --- a/doubleml/rdd/tests/test_rdd_classifier.py +++ b/doubleml/rdd/tests/test_rdd_classifier.py @@ -1,12 +1,11 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd +import pytest +from sklearn.linear_model import LogisticRegression -from doubleml.rdd.datasets import make_simple_rdd_data import doubleml as dml from doubleml.rdd import RDFlex - -from sklearn.linear_model import LogisticRegression +from doubleml.rdd.datasets import make_simple_rdd_data np.random.seed(3141) diff --git a/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py b/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py index 56be5fc02..13f94f93e 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py +++ b/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for fuzzy data """ -import pytest import numpy as np +import pytest from sklearn.dummy import DummyClassifier ml_g_dummy = DummyClassifier(strategy='constant', constant=0) diff --git a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py index ff65bff2f..5e93f368b 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py +++ b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for left sided fuzzy data """ -import pytest import numpy as np +import pytest from sklearn.dummy import DummyClassifier ml_g_dummy = DummyClassifier(strategy='constant', constant=0) diff --git a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py index 2969663bc..14527b176 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py +++ b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for right sided fuzzy data """ -import pytest import numpy as np +import pytest from sklearn.dummy import DummyClassifier ml_g_dummy = DummyClassifier(strategy='constant', constant=0) diff --git a/doubleml/rdd/tests/test_rdd_classifier_sharp.py b/doubleml/rdd/tests/test_rdd_classifier_sharp.py index b0a742a40..24b161bed 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_sharp.py +++ b/doubleml/rdd/tests/test_rdd_classifier_sharp.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for sharp data """ -import pytest import numpy as np +import pytest from sklearn.dummy import DummyClassifier ml_g_dummy = DummyClassifier(strategy='constant', constant=0) diff --git a/doubleml/rdd/tests/test_rdd_default_values.py b/doubleml/rdd/tests/test_rdd_default_values.py index edd58b3e9..3226af06a 100644 --- a/doubleml/rdd/tests/test_rdd_default_values.py +++ b/doubleml/rdd/tests/test_rdd_default_values.py @@ -1,13 +1,12 @@ -import pytest import numpy as np import pandas as pd +import pytest +from sklearn.linear_model import Lasso, LogisticRegression import doubleml as dml from doubleml.rdd import RDFlex from doubleml.rdd.datasets import make_simple_rdd_data -from sklearn.linear_model import Lasso, LogisticRegression - np.random.seed(3141) n_obs = 300 diff --git a/doubleml/rdd/tests/test_rdd_exceptions.py b/doubleml/rdd/tests/test_rdd_exceptions.py index beee396d0..39184cff6 100644 --- a/doubleml/rdd/tests/test_rdd_exceptions.py +++ b/doubleml/rdd/tests/test_rdd_exceptions.py @@ -1,14 +1,14 @@ -import pytest -import pandas as pd -import numpy as np import copy +import numpy as np +import pandas as pd +import pytest +from sklearn.base import BaseEstimator, ClassifierMixin, RegressorMixin +from sklearn.linear_model import Lasso, LogisticRegression + from doubleml import DoubleMLData -from doubleml.rdd.datasets import make_simple_rdd_data from doubleml.rdd import RDFlex - -from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin -from sklearn.linear_model import Lasso, LogisticRegression +from doubleml.rdd.datasets import make_simple_rdd_data n = 500 data = make_simple_rdd_data(n_obs=n, fuzzy=False) diff --git a/doubleml/rdd/tests/test_rdd_fuzzy.py b/doubleml/rdd/tests/test_rdd_fuzzy.py index 8e41b3c5e..88c27d729 100644 --- a/doubleml/rdd/tests/test_rdd_fuzzy.py +++ b/doubleml/rdd/tests/test_rdd_fuzzy.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for fuzzy data """ -import pytest import numpy as np +import pytest @pytest.fixture(scope='module', diff --git a/doubleml/rdd/tests/test_rdd_fuzzy_left.py b/doubleml/rdd/tests/test_rdd_fuzzy_left.py index f8ca4a3e2..8da3a18bc 100644 --- a/doubleml/rdd/tests/test_rdd_fuzzy_left.py +++ b/doubleml/rdd/tests/test_rdd_fuzzy_left.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for left sided fuzzy data """ -import pytest import numpy as np +import pytest @pytest.fixture(scope='module', diff --git a/doubleml/rdd/tests/test_rdd_fuzzy_right.py b/doubleml/rdd/tests/test_rdd_fuzzy_right.py index 627bfde91..dc8ede770 100644 --- a/doubleml/rdd/tests/test_rdd_fuzzy_right.py +++ b/doubleml/rdd/tests/test_rdd_fuzzy_right.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for right sided fuzzy data """ -import pytest import numpy as np +import pytest @pytest.fixture(scope='module', diff --git a/doubleml/rdd/tests/test_rdd_not_installed.py b/doubleml/rdd/tests/test_rdd_not_installed.py index f8e21ebc2..4e1038d44 100644 --- a/doubleml/rdd/tests/test_rdd_not_installed.py +++ b/doubleml/rdd/tests/test_rdd_not_installed.py @@ -1,6 +1,7 @@ -import pytest from unittest.mock import patch +import pytest + import doubleml as dml diff --git a/doubleml/rdd/tests/test_rdd_return_types.py b/doubleml/rdd/tests/test_rdd_return_types.py index b7a98085b..2a62d1842 100644 --- a/doubleml/rdd/tests/test_rdd_return_types.py +++ b/doubleml/rdd/tests/test_rdd_return_types.py @@ -1,13 +1,12 @@ -import pytest import numpy as np import pandas as pd +import pytest +from sklearn.linear_model import Lasso, LogisticRegression import doubleml as dml from doubleml.rdd import RDFlex from doubleml.rdd.datasets import make_simple_rdd_data -from sklearn.linear_model import Lasso, LogisticRegression - np.random.seed(3141) n_obs = 300 diff --git a/doubleml/rdd/tests/test_rdd_sharp.py b/doubleml/rdd/tests/test_rdd_sharp.py index b2b95968d..b93a69197 100644 --- a/doubleml/rdd/tests/test_rdd_sharp.py +++ b/doubleml/rdd/tests/test_rdd_sharp.py @@ -1,8 +1,8 @@ """ Dummy test using fixed learner for sharp data """ -import pytest import numpy as np +import pytest @pytest.fixture(scope='module', From 11664a24d185f76c3a77d19c6823a367a8584e7e Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:29:27 +0100 Subject: [PATCH 11/28] format rdd --- doubleml/rdd/datasets/simple_dgp.py | 20 +- doubleml/rdd/rdd.py | 212 ++++++++++-------- doubleml/rdd/tests/conftest.py | 96 ++++---- doubleml/rdd/tests/test_rdd_classifier.py | 6 +- .../rdd/tests/test_rdd_classifier_fuzzy.py | 38 ++-- .../tests/test_rdd_classifier_fuzzy_left.py | 38 ++-- .../tests/test_rdd_classifier_fuzzy_right.py | 38 ++-- .../rdd/tests/test_rdd_classifier_sharp.py | 38 ++-- doubleml/rdd/tests/test_rdd_default_values.py | 6 +- doubleml/rdd/tests/test_rdd_exceptions.py | 107 +++++---- doubleml/rdd/tests/test_rdd_fuzzy.py | 44 ++-- doubleml/rdd/tests/test_rdd_fuzzy_left.py | 44 ++-- doubleml/rdd/tests/test_rdd_fuzzy_right.py | 44 ++-- doubleml/rdd/tests/test_rdd_not_installed.py | 2 +- doubleml/rdd/tests/test_rdd_return_types.py | 10 +- doubleml/rdd/tests/test_rdd_sharp.py | 44 ++-- 16 files changed, 372 insertions(+), 415 deletions(-) diff --git a/doubleml/rdd/datasets/simple_dgp.py b/doubleml/rdd/datasets/simple_dgp.py index abfd4ed39..cdde0fc11 100644 --- a/doubleml/rdd/datasets/simple_dgp.py +++ b/doubleml/rdd/datasets/simple_dgp.py @@ -53,10 +53,10 @@ def make_simple_rdd_data(n_obs=5000, p=4, fuzzy=True, binary_outcome=False, **kw The oracle values contain the potential outcomes. """ - cutoff = kwargs.get('cutoff', 0.0) - dim_x = kwargs.get('dim_x', 3) - a = kwargs.get('a', 0.0) - tau = kwargs.get('tau', 1.0) + cutoff = kwargs.get("cutoff", 0.0) + dim_x = kwargs.get("dim_x", 3) + a = kwargs.get("a", 0.0) + tau = kwargs.get("tau", 1.0) score = np.random.normal(size=n_obs) # independent covariates @@ -95,14 +95,8 @@ def make_simple_rdd_data(n_obs=5000, p=4, fuzzy=True, binary_outcome=False, **kw Y = Y0 * (1 - D) + Y1 * D oracle_values = { - 'Y0': Y0, - 'Y1': Y1, - } - res_dict = { - 'score': score, - 'Y': Y, - 'D': D, - 'X': X, - 'oracle_values': oracle_values + "Y0": Y0, + "Y1": Y1, } + res_dict = {"score": score, "Y": Y, "D": D, "X": X, "oracle_values": oracle_values} return res_dict diff --git a/doubleml/rdd/rdd.py b/doubleml/rdd/rdd.py index c2e23f051..a8d80eb5a 100644 --- a/doubleml/rdd/rdd.py +++ b/doubleml/rdd/rdd.py @@ -17,7 +17,7 @@ rdrobust = _is_rdrobust_available() -class RDFlex(): +class RDFlex: """Flexible adjustment with double machine learning for regression discontinuity designs Parameters @@ -94,22 +94,23 @@ class RDFlex(): """ - def __init__(self, - obj_dml_data, - ml_g, - ml_m=None, - fuzzy=False, - cutoff=0, - n_folds=5, - n_rep=1, - h_fs=None, - fs_specification="cutoff", - fs_kernel="triangular", - **kwargs): + def __init__( + self, + obj_dml_data, + ml_g, + ml_m=None, + fuzzy=False, + cutoff=0, + n_folds=5, + n_rep=1, + h_fs=None, + fs_specification="cutoff", + fs_kernel="triangular", + **kwargs, + ): if rdrobust is None: - msg = ("rdrobust is not installed. " - "Please install it using 'pip install DoubleML[rdd]'") + msg = "rdrobust is not installed. " "Please install it using 'pip install DoubleML[rdd]'" raise ImportError(msg) self._check_data(obj_dml_data, cutoff) @@ -121,7 +122,7 @@ def __init__(self, self._fuzzy = fuzzy if not fuzzy and any(self._dml_data.d != self._intendend_treatment): - warnings.warn('A sharp RD design is being estimated, but the data indicate that the design is fuzzy.') + warnings.warn("A sharp RD design is being estimated, but the data indicate that the design is fuzzy.") self._check_and_set_learner(ml_g, ml_m) @@ -131,14 +132,10 @@ def __init__(self, if h_fs is None: fuzzy = self._dml_data.d if self._fuzzy else None - self._h_fs = rdrobust.rdbwselect( - y=obj_dml_data.y, - x=self._score, - fuzzy=fuzzy).bws.values.flatten().max() + self._h_fs = rdrobust.rdbwselect(y=obj_dml_data.y, x=self._score, fuzzy=fuzzy).bws.values.flatten().max() else: if not isinstance(h_fs, (float)): - raise TypeError("Initial bandwidth 'h_fs' has to be a float. " - f'Object of type {str(type(h_fs))} passed.') + raise TypeError("Initial bandwidth 'h_fs' has to be a float. " f"Object of type {str(type(h_fs))} passed.") self._h_fs = h_fs self._fs_specification = self._check_fs_specification(fs_specification) @@ -150,11 +147,11 @@ def __init__(self, # TODO: Add further input checks self.kwargs = kwargs - self._smpls = DoubleMLResampling(n_folds=self.n_folds, n_rep=self.n_rep, n_obs=obj_dml_data.n_obs, - stratify=obj_dml_data.d).split_samples() + self._smpls = DoubleMLResampling( + n_folds=self.n_folds, n_rep=self.n_rep, n_obs=obj_dml_data.n_obs, stratify=obj_dml_data.d + ).split_samples() - self._M_Y, self._M_D, self._h, self._rdd_obj, \ - self._all_coef, self._all_se, self._all_ci = self._initialize_arrays() + self._M_Y, self._M_D, self._h, self._rdd_obj, self._all_coef, self._all_se, self._all_ci = self._initialize_arrays() # Initialize all properties to None self._coef = None @@ -198,10 +195,11 @@ def __str__(self): result = "\n".join(lines) additional_info = ( - "\nDesign Type: " + ("Fuzzy" if self.fuzzy else "Sharp") + - f"\nCutoff: {self.cutoff}" + - f"\nFirst Stage Kernel: {self.fs_kernel}" + - f"\nFinal Bandwidth: {self.h}" + "\nDesign Type: " + + ("Fuzzy" if self.fuzzy else "Sharp") + + f"\nCutoff: {self.cutoff}" + + f"\nFirst Stage Kernel: {self.fs_kernel}" + + f"\nFinal Bandwidth: {self.h}" ) return result + additional_info @@ -344,13 +342,13 @@ def fit(self, n_iterations=2): weights = self.w for iteration in range(n_iterations): - eta_Y = self._fit_nuisance_model(outcome=Y, estimator_name="ml_g", - weights=weights, smpls=self._smpls[i_rep]) + eta_Y = self._fit_nuisance_model(outcome=Y, estimator_name="ml_g", weights=weights, smpls=self._smpls[i_rep]) self._M_Y[:, i_rep] = Y - eta_Y if self.fuzzy: - eta_D = self._fit_nuisance_model(outcome=D, estimator_name="ml_m", - weights=weights, smpls=self._smpls[i_rep]) + eta_D = self._fit_nuisance_model( + outcome=D, estimator_name="ml_m", weights=weights, smpls=self._smpls[i_rep] + ) self._M_D[:, i_rep] = D - eta_D # update weights via iterative bandwidth fitting @@ -384,26 +382,26 @@ def confint(self, level=0.95): A data frame with the confidence interval(s). """ if not isinstance(level, float): - raise TypeError('The confidence level must be of float type. ' - f'{str(level)} of type {str(type(level))} was passed.') + raise TypeError( + "The confidence level must be of float type. " f"{str(level)} of type {str(type(level))} was passed." + ) if (level <= 0) | (level >= 1): - raise ValueError('The confidence level must be in (0,1). ' - f'{str(level)} was passed.') + raise ValueError("The confidence level must be in (0,1). " f"{str(level)} was passed.") # compute critical values alpha = 1 - level - percentages = np.array([alpha / 2, 1. - alpha / 2]) + percentages = np.array([alpha / 2, 1.0 - alpha / 2]) critical_values = np.repeat(norm.ppf(percentages[1]), self._n_rep) # compute all cis over repetitions (shape: n_coef x 2 x n_rep) self._all_cis = np.stack( - (self.all_coef - self.all_se * critical_values, - self.all_coef + self.all_se * critical_values), - axis=1) + (self.all_coef - self.all_se * critical_values, self.all_coef + self.all_se * critical_values), axis=1 + ) ci = np.median(self._all_cis, axis=2) - df_ci = pd.DataFrame(ci, columns=['{:.1f} %'.format(i * 100) for i in percentages], - index=['Conventional', 'Bias-Corrected', 'Robust']) + df_ci = pd.DataFrame( + ci, columns=["{:.1f} %".format(i * 100) for i in percentages], index=["Conventional", "Bias-Corrected", "Robust"] + ) return df_ci @@ -422,8 +420,9 @@ def _fit_nuisance_model(self, outcome, estimator_name, weights, smpls): assert self._fs_specification == "interacted cutoff and score" Z = np.column_stack((self._intendend_treatment, self._intendend_treatment * self._score, self._score)) Z_left = np.zeros_like(Z) - Z_right = np.column_stack((np.ones_like(self._intendend_treatment), np.zeros_like(self._score), - np.zeros_like(self._score))) + Z_right = np.column_stack( + (np.ones_like(self._intendend_treatment), np.zeros_like(self._score), np.zeros_like(self._score)) + ) X = self._dml_data.x ZX = np.column_stack((Z, X)) @@ -444,7 +443,7 @@ def _fit_nuisance_model(self, outcome, estimator_name, weights, smpls): mu_left[test_index] = estimator.predict_proba(ZX_left[test_index])[:, 1] mu_right[test_index] = estimator.predict_proba(ZX_right[test_index])[:, 1] - return (mu_left + mu_right)/2 + return (mu_left + mu_right) / 2 def _update_weights(self): rdd_res = self._fit_rdd() @@ -458,12 +457,10 @@ def _update_weights(self): def _fit_rdd(self, h=None, b=None): if self.fuzzy: rdd_res = rdrobust.rdrobust( - y=self._M_Y[:, self._i_rep], x=self._score, - fuzzy=self._M_D[:, self._i_rep], h=h, b=b, **self.kwargs) + y=self._M_Y[:, self._i_rep], x=self._score, fuzzy=self._M_D[:, self._i_rep], h=h, b=b, **self.kwargs + ) else: - rdd_res = rdrobust.rdrobust( - y=self._M_Y[:, self._i_rep], x=self._score, - h=h, b=b, **self.kwargs) + rdd_res = rdrobust.rdrobust(y=self._M_Y[:, self._i_rep], x=self._score, h=h, b=b, **self.kwargs) return rdd_res def _set_coefs(self, rdd_res, h): @@ -490,79 +487,87 @@ def _initialize_arrays(self): def _check_data(self, obj_dml_data, cutoff): if not isinstance(obj_dml_data, DoubleMLData): - raise TypeError('The data must be of DoubleMLData type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) # score checks if obj_dml_data.s_col is None: - raise ValueError('Incompatible data. ' + - 'Score variable has not been set. ') - is_continuous = (type_of_target(obj_dml_data.s) == 'continuous') + raise ValueError("Incompatible data. " + "Score variable has not been set. ") + is_continuous = type_of_target(obj_dml_data.s) == "continuous" if not is_continuous: - raise ValueError('Incompatible data. ' + - 'Score variable has to be continuous. ') + raise ValueError("Incompatible data. " + "Score variable has to be continuous. ") if not isinstance(cutoff, (int, float)): - raise TypeError('Cutoff value has to be a float or int. ' - f'Object of type {str(type(cutoff))} passed.') + raise TypeError("Cutoff value has to be a float or int. " f"Object of type {str(type(cutoff))} passed.") if not (obj_dml_data.s.min() <= cutoff <= obj_dml_data.s.max()): - raise ValueError('Cutoff value is not within the range of the score variable. ') + raise ValueError("Cutoff value is not within the range of the score variable. ") # treatment checks - one_treat = (obj_dml_data.n_treat == 1) - binary_treat = (type_of_target(obj_dml_data.d) == 'binary') + one_treat = obj_dml_data.n_treat == 1 + binary_treat = type_of_target(obj_dml_data.d) == "binary" zero_one_treat = np.all((np.power(obj_dml_data.d, 2) - obj_dml_data.d) == 0) if not (one_treat & binary_treat & zero_one_treat): - raise ValueError('Incompatible data. ' - 'To fit an RDFlex model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + raise ValueError( + "Incompatible data. " + "To fit an RDFlex model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) # instrument checks if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ') + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + ) def _check_and_set_learner(self, ml_g, ml_m): # check ml_g - ml_g_is_classifier = DoubleML._check_learner(ml_g, 'ml_g', regressor=True, classifier=True) - _check_supports_sample_weights(ml_g, 'ml_g') - self._learner = {'ml_g': ml_g} + ml_g_is_classifier = DoubleML._check_learner(ml_g, "ml_g", regressor=True, classifier=True) + _check_supports_sample_weights(ml_g, "ml_g") + self._learner = {"ml_g": ml_g} if ml_g_is_classifier: if self._dml_data.binary_outcome: - self._predict_method = {'ml_g': 'predict_proba'} + self._predict_method = {"ml_g": "predict_proba"} else: - raise ValueError(f'The ml_g learner {str(ml_g)} was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + raise ValueError( + f"The ml_g learner {str(ml_g)} was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) else: - self._predict_method = {'ml_g': 'predict'} + self._predict_method = {"ml_g": "predict"} # check ml_m if self._fuzzy: if ml_m is not None: - _ = DoubleML._check_learner(ml_m, 'ml_m', regressor=False, classifier=True) - _check_supports_sample_weights(ml_m, 'ml_m') + _ = DoubleML._check_learner(ml_m, "ml_m", regressor=False, classifier=True) + _check_supports_sample_weights(ml_m, "ml_m") - self._learner['ml_m'] = ml_m - self._predict_method['ml_m'] = 'predict_proba' + self._learner["ml_m"] = ml_m + self._predict_method["ml_m"] = "predict_proba" else: - raise ValueError('Fuzzy design requires a classifier ml_m for treatment assignment.') + raise ValueError("Fuzzy design requires a classifier ml_m for treatment assignment.") else: if ml_m is not None: - warnings.warn(('A learner ml_m has been provided for for a sharp design but will be ignored. ' - 'A learner ml_m is not required for estimation.')) + warnings.warn( + ( + "A learner ml_m has been provided for for a sharp design but will be ignored. " + "A learner ml_m is not required for estimation." + ) + ) def _check_and_set_kernel(self, fs_kernel): if not isinstance(fs_kernel, (str, Callable)): - raise TypeError('fs_kernel must be either a string or a callable. ' - f'{str(fs_kernel)} of type {str(type(fs_kernel))} was passed.') + raise TypeError( + "fs_kernel must be either a string or a callable. " + f"{str(fs_kernel)} of type {str(type(fs_kernel))} was passed." + ) kernel_functions = { "uniform": lambda x, h: np.array(np.abs(x) <= h, dtype=float), "triangular": lambda x, h: np.array(np.maximum(0, (h - np.abs(x)) / h), dtype=float), - "epanechnikov": lambda x, h: np.array(np.where(np.abs(x) < h, .75 * (1 - np.square(x / h)), 0), dtype=float) + "epanechnikov": lambda x, h: np.array(np.where(np.abs(x) < h, 0.75 * (1 - np.square(x / h)), 0), dtype=float), } if isinstance(fs_kernel, str): @@ -576,36 +581,45 @@ def _check_and_set_kernel(self, fs_kernel): else: assert callable(fs_kernel) kernel_function = fs_kernel - kernel_name = 'custom_kernel' + kernel_name = "custom_kernel" return kernel_function, kernel_name def _check_fs_specification(self, fs_specification): if not isinstance(fs_specification, str): - raise TypeError("fs_specification must be a string. " - f'{str(fs_specification)} of type {str(type(fs_specification))} was passed.') + raise TypeError( + "fs_specification must be a string. " + f"{str(fs_specification)} of type {str(type(fs_specification))} was passed." + ) expected_specifications = ["cutoff", "cutoff and score", "interacted cutoff and score"] if fs_specification not in expected_specifications: - raise ValueError(f"Invalid fs_specification '{fs_specification}'. " - f"Valid specifications are {expected_specifications}.") + raise ValueError( + f"Invalid fs_specification '{fs_specification}'. " f"Valid specifications are {expected_specifications}." + ) return fs_specification def _check_iterations(self, n_iterations): """Validate the number of iterations.""" if not isinstance(n_iterations, int): - raise TypeError('The number of iterations for the iterative bandwidth fitting must be of int type. ' - f'{str(n_iterations)} of type {str(type(n_iterations))} was passed.') + raise TypeError( + "The number of iterations for the iterative bandwidth fitting must be of int type. " + f"{str(n_iterations)} of type {str(type(n_iterations))} was passed." + ) if n_iterations < 1: - raise ValueError('The number of iterations for the iterative bandwidth fitting has to be positive. ' - f'{str(n_iterations)} was passed.') + raise ValueError( + "The number of iterations for the iterative bandwidth fitting has to be positive. " + f"{str(n_iterations)} was passed." + ) def _check_effect_sign(self, tolerance=1e-6): d_left, d_right = self._dml_data.d[self._score < 0], self._dml_data.d[self._score > 0] w_left, w_right = self._w[self._score < 0], self._w[self._score > 0] treatment_prob_difference = np.average(d_left, weights=w_left) - np.average(d_right, weights=w_right) if treatment_prob_difference > tolerance: - warnings.warn("Treatment probability within bandwidth left from cutoff higher than right from cutoff.\n" - "Treatment assignment might be based on the wrong side of the cutoff.") + warnings.warn( + "Treatment probability within bandwidth left from cutoff higher than right from cutoff.\n" + "Treatment assignment might be based on the wrong side of the cutoff." + ) def aggregate_over_splits(self): var_scaling_factors = np.array([np.sum(res.N_h) for res in self._rdd_obj]) diff --git a/doubleml/rdd/tests/conftest.py b/doubleml/rdd/tests/conftest.py index b850053e0..40cfdbb2f 100644 --- a/doubleml/rdd/tests/conftest.py +++ b/doubleml/rdd/tests/conftest.py @@ -13,54 +13,42 @@ DATA_SIZE = 500 -ml_g_dummy = DummyRegressor(strategy='constant', constant=0) -ml_m_dummy = DummyClassifier(strategy='constant', constant=0) +ml_g_dummy = DummyRegressor(strategy="constant", constant=0) +ml_m_dummy = DummyClassifier(strategy="constant", constant=0) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_dummy(): """ - make predictions using rd-flex with constant model - make predictions using rdrobust as a reference """ + def _predict_dummy(data: DoubleMLData, cutoff, alpha, n_rep, p, fs_specification, ml_g=ml_g_dummy): dml_rdflex = RDFlex( - data, - ml_g=ml_g, - ml_m=ml_m_dummy, - cutoff=cutoff, - n_rep=n_rep, - p=p, - fs_specification=fs_specification + data, ml_g=ml_g, ml_m=ml_m_dummy, cutoff=cutoff, n_rep=n_rep, p=p, fs_specification=fs_specification ) dml_rdflex.fit(n_iterations=1) - ci_manual = dml_rdflex.confint(level=1-alpha) + ci_manual = dml_rdflex.confint(level=1 - alpha) if rdrobust is None: - msg = ("rdrobust is not installed. " - "Please install it using 'pip install DoubleML[rdd]'") + msg = "rdrobust is not installed. " "Please install it using 'pip install DoubleML[rdd]'" raise ImportError(msg) - rdrobust_model = rdrobust.rdrobust( - y=data.y, - x=data.s, - c=cutoff, - level=100*(1-alpha), - p=p - ) + rdrobust_model = rdrobust.rdrobust(y=data.y, x=data.s, c=cutoff, level=100 * (1 - alpha), p=p) reference = { - 'model': rdrobust_model, - 'coef': rdrobust_model.coef.values.flatten(), - 'se': rdrobust_model.se.values.flatten(), - 'ci': rdrobust_model.ci.values + "model": rdrobust_model, + "coef": rdrobust_model.coef.values.flatten(), + "se": rdrobust_model.se.values.flatten(), + "ci": rdrobust_model.ci.values, } actual = { - 'model': dml_rdflex, - 'coef': dml_rdflex.coef, - 'se': dml_rdflex.se, - 'ci': ci_manual, + "model": dml_rdflex, + "coef": dml_rdflex.coef, + "se": dml_rdflex.se, + "ci": ci_manual, } return reference, actual @@ -68,66 +56,62 @@ def _predict_dummy(data: DoubleMLData, cutoff, alpha, n_rep, p, fs_specification def defier_mask(fuzzy, data, actual_cutoff): - if fuzzy == 'left': + if fuzzy == "left": # right defiers (not treated even if score suggested it - return (data['D'] == 0) & (data['score'] >= actual_cutoff) - elif fuzzy == 'right': + return (data["D"] == 0) & (data["score"] >= actual_cutoff) + elif fuzzy == "right": # left defiers (treated even if score not suggested it - return (data['D'] == 1) & (data['score'] < actual_cutoff) - elif fuzzy in ['both', 'none']: + return (data["D"] == 1) & (data["score"] < actual_cutoff) + elif fuzzy in ["both", "none"]: return None - raise ValueError(f'Invalid type of fuzzyness {fuzzy}') + raise ValueError(f"Invalid type of fuzzyness {fuzzy}") -def generate_data( - n_obs: int, - fuzzy: str, - cutoff: float, - binary_outcome: bool = False -): +def generate_data(n_obs: int, fuzzy: str, cutoff: float, binary_outcome: bool = False): data = make_simple_rdd_data( n_obs=n_obs, - fuzzy=fuzzy in ['both', 'left', 'right'], + fuzzy=fuzzy in ["both", "left", "right"], cutoff=cutoff, binary_outcome=binary_outcome, ) mask = defier_mask(fuzzy, data, cutoff) if mask is not None: - data = {k: v[~mask] for k, v in data.items() if k != 'oracle_values'} + data = {k: v[~mask] for k, v in data.items() if k != "oracle_values"} - columns = ['y', 'd', 'score'] + ['x' + str(i) for i in range(data['X'].shape[1])] - df = pd.DataFrame( - np.column_stack((data['Y'], data['D'], data['score'], data['X'])), - columns=columns - ) - return DoubleMLData(df, y_col='y', d_cols='d', s_col='score') + columns = ["y", "d", "score"] + ["x" + str(i) for i in range(data["X"].shape[1])] + df = pd.DataFrame(np.column_stack((data["Y"], data["D"], data["score"], data["X"])), columns=columns) + return DoubleMLData(df, y_col="y", d_cols="d", s_col="score") -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def rdd_sharp_data(): def _rdd_sharp_data(cutoff, binary_outcome=False): - return generate_data(n_obs=DATA_SIZE, fuzzy='none', cutoff=cutoff, binary_outcome=binary_outcome) + return generate_data(n_obs=DATA_SIZE, fuzzy="none", cutoff=cutoff, binary_outcome=binary_outcome) + return _rdd_sharp_data -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def rdd_fuzzy_data(): def _rdd_fuzzy_data(cutoff, binary_outcome=False): - return generate_data(n_obs=DATA_SIZE, fuzzy='both', cutoff=cutoff, binary_outcome=binary_outcome) + return generate_data(n_obs=DATA_SIZE, fuzzy="both", cutoff=cutoff, binary_outcome=binary_outcome) + return _rdd_fuzzy_data -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def rdd_fuzzy_left_data(): def _rdd_fuzzy_left_data(cutoff, binary_outcome=False): - return generate_data(n_obs=DATA_SIZE, fuzzy='left', cutoff=cutoff, binary_outcome=binary_outcome) + return generate_data(n_obs=DATA_SIZE, fuzzy="left", cutoff=cutoff, binary_outcome=binary_outcome) + return _rdd_fuzzy_left_data -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def rdd_fuzzy_right_data(): def _rdd_fuzzy_right_data(cutoff, binary_outcome=False): - data = generate_data(n_obs=DATA_SIZE, fuzzy='left', cutoff=cutoff, binary_outcome=binary_outcome) + data = generate_data(n_obs=DATA_SIZE, fuzzy="left", cutoff=cutoff, binary_outcome=binary_outcome) return data + return _rdd_fuzzy_right_data diff --git a/doubleml/rdd/tests/test_rdd_classifier.py b/doubleml/rdd/tests/test_rdd_classifier.py index 19b9c53cc..199fe327d 100644 --- a/doubleml/rdd/tests/test_rdd_classifier.py +++ b/doubleml/rdd/tests/test_rdd_classifier.py @@ -15,10 +15,10 @@ df = pd.DataFrame( - np.column_stack((data['Y_bin'], data['D'], data['score'], data['X'])), - columns=['y', 'd', 'score'] + ['x' + str(i) for i in range(data['X'].shape[1])] + np.column_stack((data["Y_bin"], data["D"], data["score"], data["X"])), + columns=["y", "d", "score"] + ["x" + str(i) for i in range(data["X"].shape[1])], ) -dml_data = dml.DoubleMLData(df, y_col='y', d_cols='d', s_col='score') +dml_data = dml.DoubleMLData(df, y_col="y", d_cols="d", s_col="score") @pytest.mark.ci_rdd diff --git a/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py b/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py index 13f94f93e..34f97692e 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py +++ b/doubleml/rdd/tests/test_rdd_classifier_fuzzy.py @@ -1,61 +1,57 @@ """ Dummy test using fixed learner for fuzzy data """ + import numpy as np import pytest from sklearn.dummy import DummyClassifier -ml_g_dummy = DummyClassifier(strategy='constant', constant=0) +ml_g_dummy = DummyClassifier(strategy="constant", constant=0) -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_fuzzy_data, cutoff): return rdd_fuzzy_data(cutoff, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_fuzzy_data): return rdd_fuzzy_data(0.0, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy @@ -65,34 +61,34 @@ def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specific @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py index 5e93f368b..5fee4acfa 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py +++ b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_left.py @@ -1,61 +1,57 @@ """ Dummy test using fixed learner for left sided fuzzy data """ + import numpy as np import pytest from sklearn.dummy import DummyClassifier -ml_g_dummy = DummyClassifier(strategy='constant', constant=0) +ml_g_dummy = DummyClassifier(strategy="constant", constant=0) -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_fuzzy_left_data, cutoff): return rdd_fuzzy_left_data(cutoff, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_fuzzy_left_data): return rdd_fuzzy_left_data(0.0, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy @@ -65,34 +61,34 @@ def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specific @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py index 14527b176..f0fdd66c2 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py +++ b/doubleml/rdd/tests/test_rdd_classifier_fuzzy_right.py @@ -1,61 +1,57 @@ """ Dummy test using fixed learner for right sided fuzzy data """ + import numpy as np import pytest from sklearn.dummy import DummyClassifier -ml_g_dummy = DummyClassifier(strategy='constant', constant=0) +ml_g_dummy = DummyClassifier(strategy="constant", constant=0) -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_fuzzy_right_data, cutoff): return rdd_fuzzy_right_data(cutoff, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_fuzzy_right_data): return rdd_fuzzy_right_data(0.0, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy @@ -65,34 +61,34 @@ def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specific @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_classifier_sharp.py b/doubleml/rdd/tests/test_rdd_classifier_sharp.py index 24b161bed..f2efe736a 100644 --- a/doubleml/rdd/tests/test_rdd_classifier_sharp.py +++ b/doubleml/rdd/tests/test_rdd_classifier_sharp.py @@ -1,61 +1,57 @@ """ Dummy test using fixed learner for sharp data """ + import numpy as np import pytest from sklearn.dummy import DummyClassifier -ml_g_dummy = DummyClassifier(strategy='constant', constant=0) +ml_g_dummy = DummyClassifier(strategy="constant", constant=0) -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_sharp_data, cutoff): return rdd_sharp_data(cutoff, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_sharp_data): return rdd_sharp_data(0.0, binary_outcome=True) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): return predict_dummy( data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification, ml_g=ml_g_dummy @@ -65,34 +61,34 @@ def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specific @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_default_values.py b/doubleml/rdd/tests/test_rdd_default_values.py index 3226af06a..2f0657f15 100644 --- a/doubleml/rdd/tests/test_rdd_default_values.py +++ b/doubleml/rdd/tests/test_rdd_default_values.py @@ -12,10 +12,10 @@ n_obs = 300 data = make_simple_rdd_data(n_obs=n_obs, fuzzy=False) df = pd.DataFrame( - np.column_stack((data['Y'], data['D'], data['score'], data['X'])), - columns=['y', 'd', 'score'] + ['x' + str(i) for i in range(data['X'].shape[1])] + np.column_stack((data["Y"], data["D"], data["score"], data["X"])), + columns=["y", "d", "score"] + ["x" + str(i) for i in range(data["X"].shape[1])], ) -dml_data = dml.DoubleMLData(df, y_col='y', d_cols='d', s_col='score') +dml_data = dml.DoubleMLData(df, y_col="y", d_cols="d", s_col="score") def _assert_resampling_default_settings(dml_obj): diff --git a/doubleml/rdd/tests/test_rdd_exceptions.py b/doubleml/rdd/tests/test_rdd_exceptions.py index 39184cff6..7d3c84266 100644 --- a/doubleml/rdd/tests/test_rdd_exceptions.py +++ b/doubleml/rdd/tests/test_rdd_exceptions.py @@ -13,11 +13,11 @@ n = 500 data = make_simple_rdd_data(n_obs=n, fuzzy=False) df = pd.DataFrame( - np.column_stack((data['Y'], data['D'], data['score'], data['X'])), - columns=['y', 'd', 'score'] + ['x' + str(i) for i in range(data['X'].shape[1])] + np.column_stack((data["Y"], data["D"], data["score"], data["X"])), + columns=["y", "d", "score"] + ["x" + str(i) for i in range(data["X"].shape[1])], ) -dml_data = DoubleMLData(df, y_col='y', d_cols='d', s_col='score') +dml_data = DoubleMLData(df, y_col="y", d_cols="d", s_col="score") ml_g = Lasso() ml_m = LogisticRegression() @@ -29,6 +29,7 @@ class DummyRegressorNoSampleWeight(BaseEstimator, RegressorMixin): A dummy regressor that predicts the mean of the target values, and does not support sample weights. """ + def fit(self, X, y): self.mean_ = np.mean(y) return self @@ -42,6 +43,7 @@ class DummyClassifierNoSampleWeight(BaseEstimator, ClassifierMixin): A dummy classifier that predicts the most frequent class, and does not support sample weights. """ + def fit(self, X, y): self.classes_, self.counts_ = np.unique(y, return_counts=True) self.most_frequent_ = self.classes_[np.argmax(self.counts_)] @@ -51,10 +53,7 @@ def predict(self, X): return np.full(shape=(X.shape[0],), fill_value=self.most_frequent_) def predict_proba(self, X): - return np.column_stack( - (np.full(shape=(X.shape[0],), fill_value=1), - np.full(shape=(X.shape[0],), fill_value=0)) - ) + return np.column_stack((np.full(shape=(X.shape[0],), fill_value=1), np.full(shape=(X.shape[0],), fill_value=0))) @pytest.mark.ci_rdd @@ -65,39 +64,41 @@ def test_rdd_exception_data(): _ = RDFlex([], ml_g) # score column - msg = 'Incompatible data. Score variable has not been set. ' + msg = "Incompatible data. Score variable has not been set. " with pytest.raises(ValueError, match=msg): tmp_dml_data = copy.deepcopy(dml_data) tmp_dml_data._s_col = None _ = RDFlex(tmp_dml_data, ml_g) - msg = 'Incompatible data. Score variable has to be continuous. ' + msg = "Incompatible data. Score variable has to be continuous. " with pytest.raises(ValueError, match=msg): tmp_dml_data = copy.deepcopy(dml_data) tmp_dml_data._s = tmp_dml_data._d _ = RDFlex(tmp_dml_data, ml_g) # existing instruments - msg = r'Incompatible data. x0 have been set as instrumental variable\(s\). ' + msg = r"Incompatible data. x0 have been set as instrumental variable\(s\). " with pytest.raises(ValueError, match=msg): tmp_dml_data = copy.deepcopy(dml_data) - tmp_dml_data._z_cols = ['x0'] + tmp_dml_data._z_cols = ["x0"] _ = RDFlex(tmp_dml_data, ml_g) # treatment exceptions - msg = ('Incompatible data. ' - 'To fit an RDFlex model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. " + "To fit an RDFlex model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) # multiple treatment variables with pytest.raises(ValueError, match=msg): tmp_dml_data = copy.deepcopy(dml_data) - tmp_dml_data._d_cols = ['d', 'x0'] + tmp_dml_data._d_cols = ["d", "x0"] _ = RDFlex(tmp_dml_data, ml_g) # non-binary treatment with pytest.raises(ValueError, match=msg): tmp_dml_data = copy.deepcopy(dml_data) - tmp_dml_data.x_cols = ['x1'] # reset x to only x1 to enable setting d to x0 - tmp_dml_data.d_cols = ['x0'] + tmp_dml_data.x_cols = ["x1"] # reset x to only x1 to enable setting d to x0 + tmp_dml_data.d_cols = ["x0"] _ = RDFlex(tmp_dml_data, ml_g) @@ -107,62 +108,70 @@ def test_rdd_exception_cutoff(): with pytest.raises(TypeError, match=msg): _ = RDFlex(dml_data, ml_g, cutoff=[200]) - msg = 'Cutoff value is not within the range of the score variable. ' + msg = "Cutoff value is not within the range of the score variable. " with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g, cutoff=200) @pytest.mark.ci_rdd def test_rdd_warning_fuzzy(): - msg = 'A sharp RD design is being estimated, but the data indicate that the design is fuzzy.' + msg = "A sharp RD design is being estimated, but the data indicate that the design is fuzzy." with pytest.warns(UserWarning, match=msg): _ = RDFlex(dml_data, ml_g, cutoff=0.1) @pytest.mark.ci_rdd def test_rdd_warning_treatment_assignment(): - msg = ("Treatment probability within bandwidth left from cutoff higher than right from cutoff.\n" - "Treatment assignment might be based on the wrong side of the cutoff.") + msg = ( + "Treatment probability within bandwidth left from cutoff higher than right from cutoff.\n" + "Treatment assignment might be based on the wrong side of the cutoff." + ) with pytest.warns(UserWarning, match=msg): tmp_dml_data = copy.deepcopy(dml_data) - tmp_dml_data._s = -1.0*tmp_dml_data._s + tmp_dml_data._s = -1.0 * tmp_dml_data._s _ = RDFlex(tmp_dml_data, ml_g, ml_m, fuzzy=True) @pytest.mark.ci_rdd -@pytest.mark.filterwarnings( - "ignore:Learner provided for ml_m is probably invalid.*no classifier.*:UserWarning" -) +@pytest.mark.filterwarnings("ignore:Learner provided for ml_m is probably invalid.*no classifier.*:UserWarning") def test_rdd_exception_learner(): # ml_g - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not' - ' binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not" + " binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g=LogisticRegression()) - msg = (r"The ml_g learner DummyRegressorNoSampleWeight\(\) does not support sample weights. Please choose a learner" - " that supports sample weights.") + msg = ( + r"The ml_g learner DummyRegressorNoSampleWeight\(\) does not support sample weights. Please choose a learner" + " that supports sample weights." + ) with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g=DummyRegressorNoSampleWeight(), ml_m=ml_m) # ml_m - msg = r'Invalid learner provided for ml_m: Lasso\(\) has no method .predict_proba\(\).' + msg = r"Invalid learner provided for ml_m: Lasso\(\) has no method .predict_proba\(\)." with pytest.raises(TypeError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m=Lasso(), fuzzy=True) - msg = 'Fuzzy design requires a classifier ml_m for treatment assignment.' + msg = "Fuzzy design requires a classifier ml_m for treatment assignment." with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g, fuzzy=True) - msg = (r"The ml_m learner DummyClassifierNoSampleWeight\(\) does not support sample weights. Please choose a learner" - " that supports sample weights.") + msg = ( + r"The ml_m learner DummyClassifierNoSampleWeight\(\) does not support sample weights. Please choose a learner" + " that supports sample weights." + ) with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m=DummyClassifierNoSampleWeight(), fuzzy=True) - msg = ('A learner ml_m has been provided for for a sharp design but will be ignored. ' - 'A learner ml_m is not required for estimation.') + msg = ( + "A learner ml_m has been provided for for a sharp design but will be ignored. " + "A learner ml_m is not required for estimation." + ) with pytest.warns(UserWarning, match=msg): tmp_dml_data = copy.deepcopy(dml_data) - tmp_dml_data._data['sharp_d'] = (tmp_dml_data.s >= 0) - tmp_dml_data.d_cols = 'sharp_d' + tmp_dml_data._data["sharp_d"] = tmp_dml_data.s >= 0 + tmp_dml_data.d_cols = "sharp_d" _ = RDFlex(tmp_dml_data, ml_g, ml_m, fuzzy=False) @@ -172,7 +181,7 @@ def test_rdd_exception_resampling(): msg = r"The number of folds must be of int type. \[1\] of type was passed." with pytest.raises(TypeError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m, n_folds=[1]) - msg = 'The number of folds greater or equal to 2. 1 was passed.' + msg = "The number of folds greater or equal to 2. 1 was passed." with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m, n_folds=1) @@ -180,7 +189,7 @@ def test_rdd_exception_resampling(): msg = r"The number of repetitions for the sample splitting must be of int type. \[0\] of type was passed." with pytest.raises(TypeError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m, n_rep=[0]) - msg = 'The number of repetitions for the sample splitting has to be positive. 0 was passed.' + msg = "The number of repetitions for the sample splitting has to be positive. 0 was passed." with pytest.raises(ValueError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m, n_rep=0) @@ -192,7 +201,7 @@ def test_rdd_exception_kernel(): _ = RDFlex(dml_data, ml_g, ml_m, fs_kernel=2) msg = r"Invalid kernel 'rbf'. Valid kernels are \['uniform', 'triangular', 'epanechnikov'\]." with pytest.raises(ValueError, match=msg): - _ = RDFlex(dml_data, ml_g, ml_m, fs_kernel='rbf') + _ = RDFlex(dml_data, ml_g, ml_m, fs_kernel="rbf") @pytest.mark.ci_rdd @@ -208,20 +217,24 @@ def test_rdd_exception_fs_specification(): with pytest.raises(TypeError, match=msg): _ = RDFlex(dml_data, ml_g, ml_m, fs_specification=1) - msg = ("Invalid fs_specification 'local_constant'. " - r"Valid specifications are \['cutoff', 'cutoff and score', 'interacted cutoff and score'\].") + msg = ( + "Invalid fs_specification 'local_constant'. " + r"Valid specifications are \['cutoff', 'cutoff and score', 'interacted cutoff and score'\]." + ) with pytest.raises(ValueError, match=msg): - _ = RDFlex(dml_data, ml_g, ml_m, fs_specification='local_constant') + _ = RDFlex(dml_data, ml_g, ml_m, fs_specification="local_constant") @pytest.mark.ci_rdd def test_rdd_exception_fit(): rdd_model = RDFlex(dml_data, ml_g, ml_m) - msg = (r"The number of iterations for the iterative bandwidth fitting must be of int type. \[0\] of type " - "was passed.") + msg = ( + r"The number of iterations for the iterative bandwidth fitting must be of int type. \[0\] of type " + "was passed." + ) with pytest.raises(TypeError, match=msg): rdd_model.fit(n_iterations=[0]) - msg = 'The number of iterations for the iterative bandwidth fitting has to be positive. 0 was passed.' + msg = "The number of iterations for the iterative bandwidth fitting has to be positive. 0 was passed." with pytest.raises(ValueError, match=msg): rdd_model.fit(n_iterations=0) diff --git a/doubleml/rdd/tests/test_rdd_fuzzy.py b/doubleml/rdd/tests/test_rdd_fuzzy.py index 88c27d729..15b7d75ff 100644 --- a/doubleml/rdd/tests/test_rdd_fuzzy.py +++ b/doubleml/rdd/tests/test_rdd_fuzzy.py @@ -1,95 +1,87 @@ """ Dummy test using fixed learner for fuzzy data """ + import numpy as np import pytest -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_fuzzy_data, cutoff): return rdd_fuzzy_data(cutoff) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_fuzzy_data): return rdd_fuzzy_data(0.0) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_fuzzy_left.py b/doubleml/rdd/tests/test_rdd_fuzzy_left.py index 8da3a18bc..5f239c527 100644 --- a/doubleml/rdd/tests/test_rdd_fuzzy_left.py +++ b/doubleml/rdd/tests/test_rdd_fuzzy_left.py @@ -1,95 +1,87 @@ """ Dummy test using fixed learner for left sided fuzzy data """ + import numpy as np import pytest -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_fuzzy_left_data, cutoff): return rdd_fuzzy_left_data(cutoff) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_fuzzy_left_data): return rdd_fuzzy_left_data(0.0) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_fuzzy_right.py b/doubleml/rdd/tests/test_rdd_fuzzy_right.py index dc8ede770..7885cf520 100644 --- a/doubleml/rdd/tests/test_rdd_fuzzy_right.py +++ b/doubleml/rdd/tests/test_rdd_fuzzy_right.py @@ -1,95 +1,87 @@ """ Dummy test using fixed learner for right sided fuzzy data """ + import numpy as np import pytest -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_fuzzy_right_data, cutoff): return rdd_fuzzy_right_data(cutoff) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_fuzzy_right_data): return rdd_fuzzy_right_data(0.0) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) diff --git a/doubleml/rdd/tests/test_rdd_not_installed.py b/doubleml/rdd/tests/test_rdd_not_installed.py index 4e1038d44..b8b49cfd1 100644 --- a/doubleml/rdd/tests/test_rdd_not_installed.py +++ b/doubleml/rdd/tests/test_rdd_not_installed.py @@ -7,7 +7,7 @@ @pytest.mark.ci def test_rdrobust_import_error(): - with patch('doubleml.rdd.rdd.rdrobust', None): + with patch("doubleml.rdd.rdd.rdrobust", None): msg = r"rdrobust is not installed. Please install it using 'pip install DoubleML\[rdd\]'" with pytest.raises(ImportError, match=msg): dml.rdd.RDFlex(None, None) diff --git a/doubleml/rdd/tests/test_rdd_return_types.py b/doubleml/rdd/tests/test_rdd_return_types.py index 2a62d1842..13248afd1 100644 --- a/doubleml/rdd/tests/test_rdd_return_types.py +++ b/doubleml/rdd/tests/test_rdd_return_types.py @@ -12,16 +12,16 @@ n_obs = 300 data = make_simple_rdd_data(n_obs=n_obs) df = pd.DataFrame( - np.column_stack((data['Y'], data['D'], data['score'], data['X'])), - columns=['y', 'd', 'score'] + ['x' + str(i) for i in range(data['X'].shape[1])] + np.column_stack((data["Y"], data["D"], data["score"], data["X"])), + columns=["y", "d", "score"] + ["x" + str(i) for i in range(data["X"].shape[1])], ) -dml_data = dml.DoubleMLData(df, y_col='y', d_cols='d', s_col='score') +dml_data = dml.DoubleMLData(df, y_col="y", d_cols="d", s_col="score") def _assert_return_types(dml_obj): assert isinstance(dml_obj.n_folds, int) assert isinstance(dml_obj.n_rep, int) - assert (isinstance(dml_obj.cutoff, float) | isinstance(dml_obj.cutoff, int)) + assert isinstance(dml_obj.cutoff, float) | isinstance(dml_obj.cutoff, int) assert isinstance(dml_obj.fuzzy, bool) assert isinstance(dml_obj.fs_kernel, str) assert isinstance(dml_obj.w, np.ndarray) @@ -36,7 +36,7 @@ def _assert_return_types_after_fit(dml_obj): assert isinstance(dml_obj.__str__(), str) assert isinstance(dml_obj.n_folds, int) assert isinstance(dml_obj.n_rep, int) - assert (isinstance(dml_obj.cutoff, float) | isinstance(dml_obj.cutoff, int)) + assert isinstance(dml_obj.cutoff, float) | isinstance(dml_obj.cutoff, int) assert isinstance(dml_obj.fuzzy, bool) assert isinstance(dml_obj.fs_kernel, str) assert isinstance(dml_obj.w, np.ndarray) diff --git a/doubleml/rdd/tests/test_rdd_sharp.py b/doubleml/rdd/tests/test_rdd_sharp.py index b93a69197..d4e699e30 100644 --- a/doubleml/rdd/tests/test_rdd_sharp.py +++ b/doubleml/rdd/tests/test_rdd_sharp.py @@ -1,95 +1,87 @@ """ Dummy test using fixed learner for sharp data """ + import numpy as np import pytest -@pytest.fixture(scope='module', - params=[-0.2, 0.0, 0.4]) +@pytest.fixture(scope="module", params=[-0.2, 0.0, 0.4]) def cutoff(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1]) +@pytest.fixture(scope="module", params=[0.05, 0.1]) def alpha(request): return request.param -@pytest.fixture(scope='module', - params=[1, 4]) +@pytest.fixture(scope="module", params=[1, 4]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 2]) +@pytest.fixture(scope="module", params=[1, 2]) def p(request): return request.param -@pytest.fixture(scope='module', - params=["cutoff", "cutoff and score", "interacted cutoff and score"]) +@pytest.fixture(scope="module", params=["cutoff", "cutoff and score", "interacted cutoff and score"]) def fs_specification(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data(rdd_sharp_data, cutoff): return rdd_sharp_data(cutoff) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def data_zero(rdd_sharp_data): return rdd_sharp_data(0.0) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_placebo(predict_dummy, data_zero, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data_zero, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def predict_nonplacebo(predict_dummy, data, cutoff, alpha, p, n_rep, fs_specification): - return predict_dummy( - data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification - ) + return predict_dummy(data, cutoff=cutoff, alpha=alpha, n_rep=n_rep, p=p, fs_specification=fs_specification) @pytest.mark.ci_rdd def test_rdd_placebo_coef(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_coef(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['coef'], reference['coef'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["coef"], reference["coef"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_se(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_se(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['se'], reference['se'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["se"], reference["se"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_placebo_ci(predict_placebo): reference, actual = predict_placebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) @pytest.mark.ci_rdd def test_rdd_nonplacebo_ci(predict_nonplacebo): reference, actual = predict_nonplacebo - assert np.allclose(actual['ci'], reference['ci'], rtol=1e-9, atol=1e-4) + assert np.allclose(actual["ci"], reference["ci"], rtol=1e-9, atol=1e-4) From b55a04f93334ad99acd56aadd8d44fedcdcf16f0 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:31:39 +0100 Subject: [PATCH 12/28] run ruff on tests --- doubleml/tests/_utils.py | 6 +-- doubleml/tests/_utils_cluster.py | 6 +-- doubleml/tests/_utils_dml_cv_predict.py | 6 +-- .../_utils_doubleml_sensitivity_manual.py | 3 +- doubleml/tests/conftest.py | 9 +---- doubleml/tests/test_cv_predict.py | 5 +-- doubleml/tests/test_datasets.py | 27 +++++++++---- doubleml/tests/test_dml_data.py | 18 +++++---- doubleml/tests/test_evaluate_learner.py | 10 ++--- doubleml/tests/test_exceptions.py | 40 ++++++++++++++----- doubleml/tests/test_exceptions_ext_preds.py | 8 ++-- doubleml/tests/test_framework.py | 8 ++-- doubleml/tests/test_framework_coverage.py | 3 +- doubleml/tests/test_framework_exceptions.py | 6 ++- .../tests/test_framework_pval_corrections.py | 4 +- doubleml/tests/test_framework_sensitivity.py | 5 +-- doubleml/tests/test_model_defaults.py | 17 +++++--- doubleml/tests/test_multiway_cluster.py | 11 +++-- doubleml/tests/test_nonlinear_cluster.py | 8 ++-- doubleml/tests/test_nonlinear_score_mixin.py | 8 ++-- doubleml/tests/test_return_types.py | 35 ++++++++-------- doubleml/tests/test_scores.py | 9 ++--- doubleml/tests/test_sensitivity.py | 12 +++--- doubleml/tests/test_sensitivity_cluster.py | 5 ++- doubleml/tests/test_set_ml_nuisance_params.py | 9 ++--- doubleml/tests/test_set_sample_splitting.py | 5 +-- 26 files changed, 156 insertions(+), 127 deletions(-) diff --git a/doubleml/tests/_utils.py b/doubleml/tests/_utils.py index 18ceef883..8605bc53e 100644 --- a/doubleml/tests/_utils.py +++ b/doubleml/tests/_utils.py @@ -1,11 +1,11 @@ import numpy as np -from sklearn.model_selection import KFold, GridSearchCV, StratifiedKFold -from sklearn.base import clone import pandas as pd from scipy.stats import norm +from sklearn.base import clone +from sklearn.model_selection import GridSearchCV, KFold, StratifiedKFold -from ..utils._estimation import _var_est, _aggregate_coefs_and_ses from ..double_ml_data import DoubleMLBaseData +from ..utils._estimation import _aggregate_coefs_and_ses, _var_est class DummyDataClass(DoubleMLBaseData): diff --git a/doubleml/tests/_utils_cluster.py b/doubleml/tests/_utils_cluster.py index 090a165cc..11e4c351b 100644 --- a/doubleml/tests/_utils_cluster.py +++ b/doubleml/tests/_utils_cluster.py @@ -1,8 +1,8 @@ -import pandas as pd -import numpy as np +import itertools +import numpy as np +import pandas as pd from sklearn.model_selection import KFold -import itertools class DoubleMLMultiwayResampling: diff --git a/doubleml/tests/_utils_dml_cv_predict.py b/doubleml/tests/_utils_dml_cv_predict.py index 7691d28b3..1f8b0f0bc 100644 --- a/doubleml/tests/_utils_dml_cv_predict.py +++ b/doubleml/tests/_utils_dml_cv_predict.py @@ -1,12 +1,10 @@ import numpy as np - import scipy.sparse as sp from joblib import Parallel, delayed - from sklearn.base import clone -from sklearn.utils.validation import _num_samples +from sklearn.model_selection._validation import _check_is_permutation, _fit_and_predict from sklearn.preprocessing import LabelEncoder -from sklearn.model_selection._validation import _fit_and_predict, _check_is_permutation +from sklearn.utils.validation import _num_samples def _dml_cv_predict_ut_version(estimator, x, y, smpls=None, diff --git a/doubleml/tests/_utils_doubleml_sensitivity_manual.py b/doubleml/tests/_utils_doubleml_sensitivity_manual.py index c20e2e4bc..6a67de4a6 100644 --- a/doubleml/tests/_utils_doubleml_sensitivity_manual.py +++ b/doubleml/tests/_utils_doubleml_sensitivity_manual.py @@ -1,7 +1,8 @@ +import copy + import numpy as np import pandas as pd from scipy.stats import norm -import copy from ..utils._estimation import _aggregate_coefs_and_ses diff --git a/doubleml/tests/conftest.py b/doubleml/tests/conftest.py index 328a1211f..ffeceb3c7 100644 --- a/doubleml/tests/conftest.py +++ b/doubleml/tests/conftest.py @@ -1,15 +1,10 @@ import numpy as np import pandas as pd - import pytest - -from sklearn.datasets import make_spd_matrix -from sklearn.datasets import make_regression, make_classification - -from doubleml.datasets import make_plr_turrell2018, make_irm_data, \ - make_pliv_CHS2015 +from sklearn.datasets import make_classification, make_regression, make_spd_matrix from doubleml import DoubleMLData +from doubleml.datasets import make_irm_data, make_pliv_CHS2015, make_plr_turrell2018 def _g(x): diff --git a/doubleml/tests/test_cv_predict.py b/doubleml/tests/test_cv_predict.py index bafe1713f..6aacc25d8 100644 --- a/doubleml/tests/test_cv_predict.py +++ b/doubleml/tests/test_cv_predict.py @@ -1,12 +1,11 @@ import numpy as np import pytest - +from sklearn.linear_model import Lasso, LogisticRegression from sklearn.model_selection import KFold, train_test_split -from sklearn.linear_model import Lasso, LogisticRegression +from doubleml.utils._estimation import _dml_cv_predict from ._utils_dml_cv_predict import _dml_cv_predict_ut_version -from doubleml.utils._estimation import _dml_cv_predict @pytest.fixture(scope='module', diff --git a/doubleml/tests/test_datasets.py b/doubleml/tests/test_datasets.py index 80f71d0e6..9cff04330 100644 --- a/doubleml/tests/test_datasets.py +++ b/doubleml/tests/test_datasets.py @@ -1,12 +1,25 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd +import pytest -from doubleml import DoubleMLData, DoubleMLClusterData -from doubleml.datasets import fetch_401K, fetch_bonus, make_plr_CCDDHNR2018, make_plr_turrell2018, \ - make_irm_data, make_iivm_data, _make_pliv_data, make_pliv_CHS2015, make_pliv_multiway_cluster_CKMS2021, \ - make_did_SZ2020, make_confounded_irm_data, make_confounded_plr_data, make_heterogeneous_data, make_ssm_data, \ - make_irm_data_discrete_treatments +from doubleml import DoubleMLClusterData, DoubleMLData +from doubleml.datasets import ( + _make_pliv_data, + fetch_401K, + fetch_bonus, + make_confounded_irm_data, + make_confounded_plr_data, + make_did_SZ2020, + make_heterogeneous_data, + make_iivm_data, + make_irm_data, + make_irm_data_discrete_treatments, + make_pliv_CHS2015, + make_pliv_multiway_cluster_CKMS2021, + make_plr_CCDDHNR2018, + make_plr_turrell2018, + make_ssm_data, +) msg_inv_return_type = 'Invalid return_type.' diff --git a/doubleml/tests/test_dml_data.py b/doubleml/tests/test_dml_data.py index f9575d56a..453b42231 100644 --- a/doubleml/tests/test_dml_data.py +++ b/doubleml/tests/test_dml_data.py @@ -1,15 +1,19 @@ -import pytest import numpy as np import pandas as pd +import pytest +from sklearn.linear_model import Lasso, LogisticRegression -from doubleml import DoubleMLData, DoubleMLPLR, DoubleMLClusterData, DoubleMLDIDCS, \ - DoubleMLSSM -from doubleml.datasets import make_plr_CCDDHNR2018, _make_pliv_data, make_pliv_CHS2015, \ - make_pliv_multiway_cluster_CKMS2021, make_did_SZ2020, make_ssm_data +from doubleml import DoubleMLClusterData, DoubleMLData, DoubleMLDIDCS, DoubleMLPLR, DoubleMLSSM +from doubleml.datasets import ( + _make_pliv_data, + make_did_SZ2020, + make_pliv_CHS2015, + make_pliv_multiway_cluster_CKMS2021, + make_plr_CCDDHNR2018, + make_ssm_data, +) from doubleml.double_ml_data import DoubleMLBaseData -from sklearn.linear_model import Lasso, LogisticRegression - class DummyDataClass(DoubleMLBaseData): def __init__(self, data): diff --git a/doubleml/tests/test_evaluate_learner.py b/doubleml/tests/test_evaluate_learner.py index 4b3056b89..1e5b96496 100644 --- a/doubleml/tests/test_evaluate_learner.py +++ b/doubleml/tests/test_evaluate_learner.py @@ -1,15 +1,13 @@ -import pytest import numpy as np -import doubleml as dml -from doubleml.datasets import make_irm_data +import pytest from sklearn.base import clone - -from sklearn.linear_model import LogisticRegression, LinearRegression from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import LinearRegression, LogisticRegression +import doubleml as dml +from doubleml.datasets import make_irm_data from doubleml.utils._estimation import _logloss - np.random.seed(3141) data = make_irm_data(theta=0.5, n_obs=200, dim_x=5, return_type='DataFrame') obj_dml_data = dml.DoubleMLData(data, 'y', 'd') diff --git a/doubleml/tests/test_exceptions.py b/doubleml/tests/test_exceptions.py index cacd1edfa..4ddd1384d 100644 --- a/doubleml/tests/test_exceptions.py +++ b/doubleml/tests/test_exceptions.py @@ -1,19 +1,37 @@ -import pytest -import pandas as pd -import numpy as np import copy -from doubleml import DoubleMLPLR, DoubleMLIRM, DoubleMLIIVM, DoubleMLPLIV, DoubleMLData, \ - DoubleMLClusterData, DoubleMLPQ, DoubleMLLPQ, DoubleMLCVAR, DoubleMLQTE, DoubleMLDID, \ - DoubleMLDIDCS, DoubleMLBLP -from doubleml.datasets import make_plr_CCDDHNR2018, make_irm_data, make_pliv_CHS2015, make_iivm_data, \ - make_pliv_multiway_cluster_CKMS2021, make_did_SZ2020 +import numpy as np +import pandas as pd +import pytest +from sklearn.base import BaseEstimator +from sklearn.linear_model import Lasso, LogisticRegression + +from doubleml import ( + DoubleMLBLP, + DoubleMLClusterData, + DoubleMLCVAR, + DoubleMLData, + DoubleMLDID, + DoubleMLDIDCS, + DoubleMLIIVM, + DoubleMLIRM, + DoubleMLLPQ, + DoubleMLPLIV, + DoubleMLPLR, + DoubleMLPQ, + DoubleMLQTE, +) +from doubleml.datasets import ( + make_did_SZ2020, + make_iivm_data, + make_irm_data, + make_pliv_CHS2015, + make_pliv_multiway_cluster_CKMS2021, + make_plr_CCDDHNR2018, +) from ._utils import DummyDataClass -from sklearn.linear_model import Lasso, LogisticRegression -from sklearn.base import BaseEstimator - np.random.seed(3141) n = 100 dml_data = make_plr_CCDDHNR2018(n_obs=n) diff --git a/doubleml/tests/test_exceptions_ext_preds.py b/doubleml/tests/test_exceptions_ext_preds.py index 4a61361d4..3f6002825 100644 --- a/doubleml/tests/test_exceptions_ext_preds.py +++ b/doubleml/tests/test_exceptions_ext_preds.py @@ -1,9 +1,9 @@ import pytest -from doubleml import DoubleMLCVAR, DoubleMLQTE, DoubleMLIRM, DoubleMLData -from doubleml.datasets import make_irm_data -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor -from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier +from doubleml import DoubleMLCVAR, DoubleMLData, DoubleMLIRM, DoubleMLQTE +from doubleml.datasets import make_irm_data +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor df_irm = make_irm_data(n_obs=10, dim_x=2, theta=0.5, return_type="DataFrame") ext_predictions = {"d": {}} diff --git a/doubleml/tests/test_framework.py b/doubleml/tests/test_framework.py index 0a447420e..5d14feeca 100644 --- a/doubleml/tests/test_framework.py +++ b/doubleml/tests/test_framework.py @@ -1,13 +1,13 @@ -import pytest import numpy as np import pandas as pd +import pytest +from sklearn.linear_model import LinearRegression, LogisticRegression from doubleml.datasets import make_irm_data -from doubleml.irm.irm import DoubleMLIRM from doubleml.double_ml_framework import DoubleMLFramework, concat -from ._utils import generate_dml_dict +from doubleml.irm.irm import DoubleMLIRM -from sklearn.linear_model import LinearRegression, LogisticRegression +from ._utils import generate_dml_dict @pytest.fixture(scope='module', diff --git a/doubleml/tests/test_framework_coverage.py b/doubleml/tests/test_framework_coverage.py index 3fe0b6498..11d658a55 100644 --- a/doubleml/tests/test_framework_coverage.py +++ b/doubleml/tests/test_framework_coverage.py @@ -1,7 +1,8 @@ -import pytest import numpy as np +import pytest from doubleml.double_ml_framework import DoubleMLFramework, concat + from ._utils import generate_dml_dict diff --git a/doubleml/tests/test_framework_exceptions.py b/doubleml/tests/test_framework_exceptions.py index b80cfac27..12e7b2d33 100644 --- a/doubleml/tests/test_framework_exceptions.py +++ b/doubleml/tests/test_framework_exceptions.py @@ -1,8 +1,10 @@ -import pytest -import numpy as np import copy +import numpy as np +import pytest + from doubleml.double_ml_framework import DoubleMLFramework, concat + from ._utils import generate_dml_dict n_obs = 10 diff --git a/doubleml/tests/test_framework_pval_corrections.py b/doubleml/tests/test_framework_pval_corrections.py index fe311f3c2..06527defa 100644 --- a/doubleml/tests/test_framework_pval_corrections.py +++ b/doubleml/tests/test_framework_pval_corrections.py @@ -1,8 +1,8 @@ -import pytest - import numpy as np +import pytest from doubleml.double_ml_framework import DoubleMLFramework + from ._utils import generate_dml_dict diff --git a/doubleml/tests/test_framework_sensitivity.py b/doubleml/tests/test_framework_sensitivity.py index 044d89d22..e102eb2d5 100644 --- a/doubleml/tests/test_framework_sensitivity.py +++ b/doubleml/tests/test_framework_sensitivity.py @@ -1,9 +1,8 @@ import pytest +from sklearn.linear_model import LinearRegression, LogisticRegression -from doubleml.irm.irm import DoubleMLIRM from doubleml.double_ml_framework import concat - -from sklearn.linear_model import LinearRegression, LogisticRegression +from doubleml.irm.irm import DoubleMLIRM @pytest.fixture(scope='module', diff --git a/doubleml/tests/test_model_defaults.py b/doubleml/tests/test_model_defaults.py index 8d7234d62..a592fcb26 100644 --- a/doubleml/tests/test_model_defaults.py +++ b/doubleml/tests/test_model_defaults.py @@ -1,12 +1,17 @@ -import pytest import numpy as np +import pytest +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import Lasso, LogisticRegression import doubleml as dml -from doubleml.datasets import make_plr_CCDDHNR2018, make_irm_data, make_pliv_CHS2015, make_iivm_data, make_did_SZ2020, \ - make_ssm_data - -from sklearn.linear_model import Lasso, LogisticRegression -from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from doubleml.datasets import ( + make_did_SZ2020, + make_iivm_data, + make_irm_data, + make_pliv_CHS2015, + make_plr_CCDDHNR2018, + make_ssm_data, +) np.random.seed(3141) dml_data_plr = make_plr_CCDDHNR2018(n_obs=100) diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index 85980f8f0..1ffd994a5 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -1,17 +1,16 @@ -import numpy as np -import pytest import math -from sklearn.linear_model import LinearRegression, Lasso +import numpy as np +import pytest from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import Lasso, LinearRegression import doubleml as dml from doubleml.datasets import make_pliv_multiway_cluster_CKMS2021 +from ..plm.tests._utils_pliv_manual import compute_pliv_residuals, fit_pliv from ._utils import _clone -from ._utils_cluster import var_one_way_cluster, est_one_way_cluster_dml2, \ - est_two_way_cluster_dml2, var_two_way_cluster -from ..plm.tests._utils_pliv_manual import fit_pliv, compute_pliv_residuals +from ._utils_cluster import est_one_way_cluster_dml2, est_two_way_cluster_dml2, var_one_way_cluster, var_two_way_cluster np.random.seed(1234) # Set the simulation parameters diff --git a/doubleml/tests/test_nonlinear_cluster.py b/doubleml/tests/test_nonlinear_cluster.py index 5bcfaace5..2c9555c00 100644 --- a/doubleml/tests/test_nonlinear_cluster.py +++ b/doubleml/tests/test_nonlinear_cluster.py @@ -1,13 +1,13 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone -from sklearn.linear_model import LinearRegression, Lasso from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import Lasso, LinearRegression import doubleml as dml -from doubleml.datasets import make_pliv_multiway_cluster_CKMS2021, DoubleMLClusterData +from doubleml.datasets import DoubleMLClusterData, make_pliv_multiway_cluster_CKMS2021 from .test_nonlinear_score_mixin import DoubleMLPLRWithNonLinearScoreMixin diff --git a/doubleml/tests/test_nonlinear_score_mixin.py b/doubleml/tests/test_nonlinear_score_mixin.py index e59dc7f01..62e834887 100644 --- a/doubleml/tests/test_nonlinear_score_mixin.py +++ b/doubleml/tests/test_nonlinear_score_mixin.py @@ -1,16 +1,16 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.base import clone from sklearn.linear_model import LinearRegression from sklearn.utils import check_X_y import doubleml as dml from doubleml.double_ml import DoubleML -from doubleml.utils._estimation import _dml_cv_predict -from doubleml.utils._checks import _check_finite_predictions from doubleml.double_ml_score_mixins import NonLinearScoreMixin +from doubleml.utils._checks import _check_finite_predictions +from doubleml.utils._estimation import _dml_cv_predict class DoubleMLPLRWithNonLinearScoreMixin(NonLinearScoreMixin, DoubleML): diff --git a/doubleml/tests/test_return_types.py b/doubleml/tests/test_return_types.py index a9014d089..e8c573609 100644 --- a/doubleml/tests/test_return_types.py +++ b/doubleml/tests/test_return_types.py @@ -1,39 +1,38 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd import plotly +import pytest +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor +from sklearn.linear_model import Lasso, LogisticRegression +from sklearn.svm import LinearSVR from doubleml import ( - DoubleMLPLR, - DoubleMLIRM, - DoubleMLIIVM, - DoubleMLPLIV, - DoubleMLData, + DoubleMLAPO, DoubleMLClusterData, DoubleMLCVAR, - DoubleMLPQ, - DoubleMLLPQ, + DoubleMLData, DoubleMLDID, DoubleMLDIDCS, - DoubleMLPolicyTree, DoubleMLFramework, + DoubleMLIIVM, + DoubleMLIRM, + DoubleMLLPQ, + DoubleMLPLIV, + DoubleMLPLR, + DoubleMLPolicyTree, + DoubleMLPQ, DoubleMLSSM, - DoubleMLAPO ) from doubleml.datasets import ( - make_plr_CCDDHNR2018, + make_did_SZ2020, + make_iivm_data, make_irm_data, make_pliv_CHS2015, - make_iivm_data, make_pliv_multiway_cluster_CKMS2021, - make_did_SZ2020, + make_plr_CCDDHNR2018, make_ssm_data, ) -from sklearn.linear_model import Lasso, LogisticRegression -from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor -from sklearn.svm import LinearSVR - np.random.seed(3141) n_obs = 200 dml_data_plr = make_plr_CCDDHNR2018(n_obs=n_obs) diff --git a/doubleml/tests/test_scores.py b/doubleml/tests/test_scores.py index 5b5c68edb..19b554190 100644 --- a/doubleml/tests/test_scores.py +++ b/doubleml/tests/test_scores.py @@ -1,11 +1,10 @@ -import pytest import numpy as np - -from doubleml import DoubleMLPLR, DoubleMLIRM, DoubleMLIIVM, DoubleMLPLIV -from doubleml.datasets import make_plr_CCDDHNR2018, make_irm_data, make_pliv_CHS2015, make_iivm_data - +import pytest from sklearn.linear_model import Lasso, LogisticRegression +from doubleml import DoubleMLIIVM, DoubleMLIRM, DoubleMLPLIV, DoubleMLPLR +from doubleml.datasets import make_iivm_data, make_irm_data, make_pliv_CHS2015, make_plr_CCDDHNR2018 + np.random.seed(3141) dml_data_plr = make_plr_CCDDHNR2018(n_obs=100) dml_data_pliv = make_pliv_CHS2015(n_obs=100, dim_z=1) diff --git a/doubleml/tests/test_sensitivity.py b/doubleml/tests/test_sensitivity.py index 9c9ca9f36..f5008d0bb 100644 --- a/doubleml/tests/test_sensitivity.py +++ b/doubleml/tests/test_sensitivity.py @@ -1,14 +1,14 @@ -import pytest -import numpy as np import copy +import numpy as np +import pytest +from sklearn.linear_model import LinearRegression, LogisticRegression + import doubleml as dml -from doubleml import DoubleMLIRM, DoubleMLData +from doubleml import DoubleMLData, DoubleMLIRM from doubleml.datasets import make_irm_data -from sklearn.linear_model import LinearRegression, LogisticRegression -from ._utils_doubleml_sensitivity_manual import doubleml_sensitivity_manual, \ - doubleml_sensitivity_benchmark_manual +from ._utils_doubleml_sensitivity_manual import doubleml_sensitivity_benchmark_manual, doubleml_sensitivity_manual @pytest.fixture(scope="module", params=[["X1"], ["X2"], ["X3"]]) diff --git a/doubleml/tests/test_sensitivity_cluster.py b/doubleml/tests/test_sensitivity_cluster.py index 9485e7373..20e7d635f 100644 --- a/doubleml/tests/test_sensitivity_cluster.py +++ b/doubleml/tests/test_sensitivity_cluster.py @@ -1,11 +1,12 @@ -import numpy as np -import pytest import math +import numpy as np +import pytest from sklearn.linear_model import LinearRegression import doubleml as dml from doubleml.datasets import make_pliv_multiway_cluster_CKMS2021 + from ._utils_doubleml_sensitivity_manual import doubleml_sensitivity_benchmark_manual np.random.seed(1234) diff --git a/doubleml/tests/test_set_ml_nuisance_params.py b/doubleml/tests/test_set_ml_nuisance_params.py index bab5f5c7a..ac2953b70 100644 --- a/doubleml/tests/test_set_ml_nuisance_params.py +++ b/doubleml/tests/test_set_ml_nuisance_params.py @@ -1,10 +1,9 @@ -import pytest import numpy as np +import pytest +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor -from doubleml import DoubleMLPLR, DoubleMLIRM, DoubleMLIIVM, DoubleMLPLIV, DoubleMLCVAR, DoubleMLPQ, DoubleMLLPQ -from doubleml.datasets import make_plr_CCDDHNR2018, make_irm_data, make_pliv_CHS2015, make_iivm_data - -from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier +from doubleml import DoubleMLCVAR, DoubleMLIIVM, DoubleMLIRM, DoubleMLLPQ, DoubleMLPLIV, DoubleMLPLR, DoubleMLPQ +from doubleml.datasets import make_iivm_data, make_irm_data, make_pliv_CHS2015, make_plr_CCDDHNR2018 # set default and test values n_est_default = 100 diff --git a/doubleml/tests/test_set_sample_splitting.py b/doubleml/tests/test_set_sample_splitting.py index 4a1474a74..771198553 100644 --- a/doubleml/tests/test_set_sample_splitting.py +++ b/doubleml/tests/test_set_sample_splitting.py @@ -1,11 +1,10 @@ -import pytest import numpy as np +import pytest +from sklearn.linear_model import Lasso from doubleml import DoubleMLPLR from doubleml.datasets import make_plr_CCDDHNR2018 -from sklearn.linear_model import Lasso - np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=10) n_obs = dml_data.n_obs From 74b6b3fee175da2e68437d925ca49e67c0edb726 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:40:45 +0100 Subject: [PATCH 13/28] format tests --- doubleml/tests/_utils.py | 43 +- doubleml/tests/_utils_boot.py | 14 +- doubleml/tests/_utils_cluster.py | 64 +- doubleml/tests/_utils_dml_cv_predict.py | 54 +- .../_utils_doubleml_sensitivity_manual.py | 45 +- doubleml/tests/conftest.py | 73 +- doubleml/tests/test_cv_predict.py | 56 +- doubleml/tests/test_datasets.py | 213 ++- doubleml/tests/test_dml_data.py | 628 +++++---- doubleml/tests/test_evaluate_learner.py | 71 +- doubleml/tests/test_exceptions.py | 1157 +++++++++-------- doubleml/tests/test_framework.py | 250 ++-- doubleml/tests/test_framework_coverage.py | 250 ++-- doubleml/tests/test_framework_exceptions.py | 174 +-- .../tests/test_framework_pval_corrections.py | 64 +- doubleml/tests/test_framework_sensitivity.py | 79 +- doubleml/tests/test_model_defaults.py | 70 +- doubleml/tests/test_multiway_cluster.py | 301 ++--- doubleml/tests/test_nonlinear_cluster.py | 202 ++- doubleml/tests/test_nonlinear_score_mixin.py | 234 ++-- doubleml/tests/test_package.py | 1 + doubleml/tests/test_return_types.py | 372 +++--- doubleml/tests/test_scores.py | 221 ++-- doubleml/tests/test_sensitivity.py | 120 +- doubleml/tests/test_sensitivity_cluster.py | 114 +- doubleml/tests/test_set_ml_nuisance_params.py | 50 +- doubleml/tests/test_set_sample_splitting.py | 180 ++- 27 files changed, 2523 insertions(+), 2577 deletions(-) diff --git a/doubleml/tests/_utils.py b/doubleml/tests/_utils.py index 8605bc53e..eeeaab3d6 100644 --- a/doubleml/tests/_utils.py +++ b/doubleml/tests/_utils.py @@ -9,8 +9,7 @@ class DummyDataClass(DoubleMLBaseData): - def __init__(self, - data): + def __init__(self, data): DoubleMLBaseData.__init__(self, data) @property @@ -22,11 +21,9 @@ def draw_smpls(n_obs, n_folds, n_rep=1, groups=None): all_smpls = [] for _ in range(n_rep): if groups is None: - resampling = KFold(n_splits=n_folds, - shuffle=True) + resampling = KFold(n_splits=n_folds, shuffle=True) else: - resampling = StratifiedKFold(n_splits=n_folds, - shuffle=True) + resampling = StratifiedKFold(n_splits=n_folds, shuffle=True) smpls = [(train, test) for train, test in resampling.split(X=np.zeros(n_obs), y=groups)] all_smpls.append(smpls) return all_smpls @@ -69,8 +66,7 @@ def tune_grid_search(y, x, ml_model, smpls, param_grid, n_folds_tune, train_cond tune_res = [None] * len(smpls) for idx, (train_index, _) in enumerate(smpls): g_tune_resampling = KFold(n_splits=n_folds_tune, shuffle=True) - g_grid_search = GridSearchCV(ml_model, param_grid, - cv=g_tune_resampling) + g_grid_search = GridSearchCV(ml_model, param_grid, cv=g_tune_resampling) if train_cond is None: tune_res[idx] = g_grid_search.fit(x[train_index, :], y[train_index]) else: @@ -93,17 +89,12 @@ def generate_dml_dict(psi_a, psi_b): n_thetas = psi_a.shape[1] n_rep = psi_a.shape[2] - all_thetas = -1.0*np.mean(psi_b, axis=0) + all_thetas = -1.0 * np.mean(psi_b, axis=0) all_ses = np.zeros(shape=(n_thetas, n_rep)) for i_rep in range(n_rep): for i_theta in range(n_thetas): - psi = psi_a[:, i_theta, i_rep]*all_thetas[i_theta, i_rep] + psi_b[:, i_theta, i_rep] - var_estimate, _ = _var_est( - psi=psi, - psi_deriv=psi_a[:, i_theta, i_rep], - smpls=None, - is_cluster_data=False - ) + psi = psi_a[:, i_theta, i_rep] * all_thetas[i_theta, i_rep] + psi_b[:, i_theta, i_rep] + var_estimate, _ = _var_est(psi=psi, psi_deriv=psi_a[:, i_theta, i_rep], smpls=None, is_cluster_data=False) all_ses[i_theta, i_rep] = np.sqrt(var_estimate) var_scaling_factors = np.full(n_thetas, n_obs) @@ -115,20 +106,20 @@ def generate_dml_dict(psi_a, psi_b): scaled_psi = psi_b / np.mean(psi_a, axis=0) doubleml_dict = { - 'thetas': thetas, - 'ses': ses, - 'all_thetas': all_thetas, - 'all_ses': all_ses, - 'var_scaling_factors': var_scaling_factors, - 'scaled_psi': scaled_psi, + "thetas": thetas, + "ses": ses, + "all_thetas": all_thetas, + "all_ses": all_ses, + "var_scaling_factors": var_scaling_factors, + "scaled_psi": scaled_psi, } return doubleml_dict def confint_manual(coef, se, index_names, boot_t_stat=None, joint=True, level=0.95): - a = (1 - level) - ab = np.array([a / 2, 1. - a / 2]) + a = 1 - level + ab = np.array([a / 2, 1.0 - a / 2]) if joint: assert boot_t_stat.shape[2] == 1 sim = np.amax(np.abs(boot_t_stat[:, :, 0]), 1) @@ -138,7 +129,5 @@ def confint_manual(coef, se, index_names, boot_t_stat=None, joint=True, level=0. fac = norm.ppf(ab) ci = np.vstack((coef + se * fac[0], coef + se * fac[1])).T - df_ci = pd.DataFrame(ci, - columns=['{:.1f} %'.format(i * 100) for i in ab], - index=index_names) + df_ci = pd.DataFrame(ci, columns=["{:.1f} %".format(i * 100) for i in ab], index=index_names) return df_ci diff --git a/doubleml/tests/_utils_boot.py b/doubleml/tests/_utils_boot.py index 191259f39..2d6113d3f 100644 --- a/doubleml/tests/_utils_boot.py +++ b/doubleml/tests/_utils_boot.py @@ -2,12 +2,12 @@ def draw_weights(method, n_rep_boot, n_obs): - if method == 'Bayes': - weights = np.random.exponential(scale=1.0, size=(n_rep_boot, n_obs)) - 1. - elif method == 'normal': + if method == "Bayes": + weights = np.random.exponential(scale=1.0, size=(n_rep_boot, n_obs)) - 1.0 + elif method == "normal": weights = np.random.normal(loc=0.0, scale=1.0, size=(n_rep_boot, n_obs)) else: - assert method == 'wild' + assert method == "wild" xx = np.random.normal(loc=0.0, scale=1.0, size=(n_rep_boot, n_obs)) yy = np.random.normal(loc=0.0, scale=1.0, size=(n_rep_boot, n_obs)) weights = xx / np.sqrt(2) + (np.power(yy, 2) - 1) / 2 @@ -20,11 +20,9 @@ def boot_manual(psi, J, smpls, se, weights, n_rep, apply_cross_fitting=True): for i_rep in range(n_rep): this_weights = weights[i_rep, :] if apply_cross_fitting: - boot_t_stat[i_rep] = np.mean(np.multiply(np.divide(this_weights, se), - psi / J)) + boot_t_stat[i_rep] = np.mean(np.multiply(np.divide(this_weights, se), psi / J)) else: test_index = smpls[0][1] - boot_t_stat[i_rep] = np.mean(np.multiply(np.divide(this_weights, se), - psi[test_index] / J)) + boot_t_stat[i_rep] = np.mean(np.multiply(np.divide(this_weights, se), psi[test_index] / J)) return boot_t_stat diff --git a/doubleml/tests/_utils_cluster.py b/doubleml/tests/_utils_cluster.py index 11e4c351b..425796cd4 100644 --- a/doubleml/tests/_utils_cluster.py +++ b/doubleml/tests/_utils_cluster.py @@ -6,12 +6,10 @@ class DoubleMLMultiwayResampling: - def __init__(self, - n_folds, - smpl_sizes): + def __init__(self, n_folds, smpl_sizes): self.n_folds = n_folds self.smpl_sizes = smpl_sizes - assert len(smpl_sizes), 'For DoubleMLMultiwayResampling mmultiple sample sizes need to be provided' + assert len(smpl_sizes), "For DoubleMLMultiwayResampling mmultiple sample sizes need to be provided" self.n_ways = len(smpl_sizes) self.resampling = KFold(n_splits=n_folds, shuffle=True) @@ -27,28 +25,28 @@ def split_samples(self): smpls.append([(train, test) for train, test in self.resampling.split(np.zeros(self.smpl_sizes[i_way]))]) smpls_multi_ind = [] - xx = n_ways*[range(self.n_folds)] + xx = n_ways * [range(self.n_folds)] for ind_index_set in itertools.product(*xx): smpls_train_list = [smpls[i][ind_index_set[i]][0] for i in range(n_ways)] smpls_test_list = [smpls[i][ind_index_set[i]][1] for i in range(n_ways)] - smpls_multi_ind.append((pd.MultiIndex.from_product(smpls_train_list).values, - pd.MultiIndex.from_product(smpls_test_list).values)) + smpls_multi_ind.append( + (pd.MultiIndex.from_product(smpls_train_list).values, pd.MultiIndex.from_product(smpls_test_list).values) + ) - smpls_lin_ind = [(multi_to_lin_ind.loc[x[0]].values, - multi_to_lin_ind.loc[x[1]].values) for x in smpls_multi_ind] + smpls_lin_ind = [(multi_to_lin_ind.loc[x[0]].values, multi_to_lin_ind.loc[x[1]].values) for x in smpls_multi_ind] return smpls_multi_ind, smpls_lin_ind def est_one_way_cluster_dml2(psi_a, psi_b, cluster_var, smpls_one_split): - psi_a_subsample = 0. - psi_b_subsample = 0. - for (_, test_index) in smpls_one_split: + psi_a_subsample = 0.0 + psi_b_subsample = 0.0 + for _, test_index in smpls_one_split: I_k = np.unique(cluster_var[test_index]) - const = 1/len(I_k) - psi_a_subsample += const*np.sum(psi_a[test_index]) - psi_b_subsample += const*np.sum(psi_b[test_index]) + const = 1 / len(I_k) + psi_a_subsample += const * np.sum(psi_a[test_index]) + psi_b_subsample += const * np.sum(psi_b[test_index]) theta = -psi_b_subsample / psi_a_subsample return theta @@ -56,31 +54,31 @@ def est_one_way_cluster_dml2(psi_a, psi_b, cluster_var, smpls_one_split): def var_one_way_cluster(psi, psi_a, cluster_var, smpls_one_split): gamma_hat = 0 j_hat = 0 - for (_, test_index) in smpls_one_split: + for _, test_index in smpls_one_split: I_k = np.unique(cluster_var[test_index]) - const = 1/len(I_k) + const = 1 / len(I_k) for i in I_k: ind = cluster_var == i for val_i in psi[ind]: for val_j in psi[ind]: gamma_hat += const * val_i * val_j - j_hat += np.sum(psi_a[test_index])/len(I_k) + j_hat += np.sum(psi_a[test_index]) / len(I_k) n_folds = len(smpls_one_split) - gamma_hat = gamma_hat/n_folds - j_hat = j_hat/n_folds - var = gamma_hat / (j_hat ** 2) / len(np.unique(cluster_var)) + gamma_hat = gamma_hat / n_folds + j_hat = j_hat / n_folds + var = gamma_hat / (j_hat**2) / len(np.unique(cluster_var)) return var def est_two_way_cluster_dml2(psi_a, psi_b, cluster_var1, cluster_var2, smpls_one_split): - psi_a_subsample = 0. - psi_b_subsample = 0. - for (_, test_index) in smpls_one_split: + psi_a_subsample = 0.0 + psi_b_subsample = 0.0 + for _, test_index in smpls_one_split: I_k = np.unique(cluster_var1[test_index]) J_l = np.unique(cluster_var2[test_index]) - const = 1/(len(I_k) * len(J_l)) - psi_a_subsample += const*np.sum(psi_a[test_index]) - psi_b_subsample += const*np.sum(psi_b[test_index]) + const = 1 / (len(I_k) * len(J_l)) + psi_a_subsample += const * np.sum(psi_a[test_index]) + psi_b_subsample += const * np.sum(psi_b[test_index]) theta = -psi_b_subsample / psi_a_subsample return theta @@ -88,10 +86,10 @@ def est_two_way_cluster_dml2(psi_a, psi_b, cluster_var1, cluster_var2, smpls_one def var_two_way_cluster(psi, psi_a, cluster_var1, cluster_var2, smpls_one_split): gamma_hat = 0 j_hat = 0 - for (_, test_index) in smpls_one_split: + for _, test_index in smpls_one_split: I_k = np.unique(cluster_var1[test_index]) J_l = np.unique(cluster_var2[test_index]) - const = min(len(I_k), len(J_l))/(len(I_k)*len(J_l))**2 + const = min(len(I_k), len(J_l)) / (len(I_k) * len(J_l)) ** 2 for i in I_k: for j in J_l: for j_ in J_l: @@ -104,12 +102,12 @@ def var_two_way_cluster(psi, psi_a, cluster_var1, cluster_var2, smpls_one_split) ind1 = (cluster_var1 == i) & (cluster_var2 == j) ind2 = (cluster_var1 == i_) & (cluster_var2 == j) gamma_hat += const * psi[ind1] * psi[ind2] - j_hat += np.sum(psi_a[test_index])/(len(I_k)*len(J_l)) + j_hat += np.sum(psi_a[test_index]) / (len(I_k) * len(J_l)) n_folds = len(smpls_one_split) - gamma_hat = gamma_hat/n_folds - j_hat = j_hat/n_folds + gamma_hat = gamma_hat / n_folds + j_hat = j_hat / n_folds n_clusters1 = len(np.unique(cluster_var1)) n_clusters2 = len(np.unique(cluster_var2)) var_scaling_factor = min(n_clusters1, n_clusters2) - var = gamma_hat / (j_hat ** 2) / var_scaling_factor + var = gamma_hat / (j_hat**2) / var_scaling_factor return var diff --git a/doubleml/tests/_utils_dml_cv_predict.py b/doubleml/tests/_utils_dml_cv_predict.py index 1f8b0f0bc..8b65854fa 100644 --- a/doubleml/tests/_utils_dml_cv_predict.py +++ b/doubleml/tests/_utils_dml_cv_predict.py @@ -7,8 +7,7 @@ from sklearn.utils.validation import _num_samples -def _dml_cv_predict_ut_version(estimator, x, y, smpls=None, - n_jobs=None, est_params=None, method='predict'): +def _dml_cv_predict_ut_version(estimator, x, y, smpls=None, n_jobs=None, est_params=None, method="predict"): # this is an adapted version of the sklearn function cross_val_predict which allows to set fold-specific parameters # original https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/model_selection/_validation.py @@ -20,23 +19,19 @@ def _dml_cv_predict_ut_version(estimator, x, y, smpls=None, train_index, test_index = smpls[0] # set some defaults aligned with cross_val_predict fit_params = None - if method == 'predict_proba': + if method == "predict_proba": predictions = np.full((len(y), 2), np.nan) else: predictions = np.full(len(y), np.nan) if est_params is None: - xx = _fit_and_predict( - clone(estimator), - x, y, train_index, test_index, fit_params, method) + xx = _fit_and_predict(clone(estimator), x, y, train_index, test_index, fit_params, method) else: assert isinstance(est_params, dict) - xx = _fit_and_predict( - clone(estimator).set_params(**est_params), - x, y, train_index, test_index, fit_params, method) + xx = _fit_and_predict(clone(estimator).set_params(**est_params), x, y, train_index, test_index, fit_params, method) # implementation is (also at other parts) restricted to a sorted set of test_indices, but this could be fixed # inv_test_indices = np.argsort(test_indices) - assert np.all(np.diff(test_indices) > 0), 'test_indices not sorted' + assert np.all(np.diff(test_indices) > 0), "test_indices not sorted" if isinstance(xx, np.ndarray): # this is sklearn >= 0.24 predictions[test_indices] = xx @@ -47,36 +42,39 @@ def _dml_cv_predict_ut_version(estimator, x, y, smpls=None, # set some defaults aligned with cross_val_predict fit_params = None verbose = 0 - pre_dispatch = '2*n_jobs' + pre_dispatch = "2*n_jobs" - encode = (method == 'predict_proba') + encode = method == "predict_proba" if encode: y = np.asarray(y) le = LabelEncoder() y = le.fit_transform(y) - parallel = Parallel(n_jobs=n_jobs, verbose=verbose, - pre_dispatch=pre_dispatch) + parallel = Parallel(n_jobs=n_jobs, verbose=verbose, pre_dispatch=pre_dispatch) # FixMe: Find a better way to handle the different combinations of paramters and smpls_is_partition if est_params is None: - prediction_blocks = parallel(delayed(_fit_and_predict)( - estimator, - x, y, train_index, test_index, fit_params, method) - for idx, (train_index, test_index) in enumerate(smpls)) + prediction_blocks = parallel( + delayed(_fit_and_predict)(estimator, x, y, train_index, test_index, fit_params, method) + for idx, (train_index, test_index) in enumerate(smpls) + ) elif isinstance(est_params, dict): # if no fold-specific parameters we redirect to the standard method # warnings.warn("Using the same (hyper-)parameters for all folds") - prediction_blocks = parallel(delayed(_fit_and_predict)( - clone(estimator).set_params(**est_params), - x, y, train_index, test_index, fit_params, method) - for idx, (train_index, test_index) in enumerate(smpls)) + prediction_blocks = parallel( + delayed(_fit_and_predict)( + clone(estimator).set_params(**est_params), x, y, train_index, test_index, fit_params, method + ) + for idx, (train_index, test_index) in enumerate(smpls) + ) else: - assert len(est_params) == len(smpls), 'provide one parameter setting per fold' - prediction_blocks = parallel(delayed(_fit_and_predict)( - clone(estimator).set_params(**est_params[idx]), - x, y, train_index, test_index, fit_params, method) - for idx, (train_index, test_index) in enumerate(smpls)) + assert len(est_params) == len(smpls), "provide one parameter setting per fold" + prediction_blocks = parallel( + delayed(_fit_and_predict)( + clone(estimator).set_params(**est_params[idx]), x, y, train_index, test_index, fit_params, method + ) + for idx, (train_index, test_index) in enumerate(smpls) + ) # Concatenate the predictions if isinstance(prediction_blocks[0], np.ndarray): @@ -86,7 +84,7 @@ def _dml_cv_predict_ut_version(estimator, x, y, smpls=None, predictions = [pred_block_i for pred_block_i, _ in prediction_blocks] if not _check_is_permutation(test_indices, _num_samples(x)): - raise ValueError('_dml_cross_val_predict only works for partitions') + raise ValueError("_dml_cross_val_predict only works for partitions") inv_test_indices = np.empty(len(test_indices), dtype=int) inv_test_indices[test_indices] = np.arange(len(test_indices)) diff --git a/doubleml/tests/_utils_doubleml_sensitivity_manual.py b/doubleml/tests/_utils_doubleml_sensitivity_manual.py index 6a67de4a6..f44bc2486 100644 --- a/doubleml/tests/_utils_doubleml_sensitivity_manual.py +++ b/doubleml/tests/_utils_doubleml_sensitivity_manual.py @@ -10,13 +10,13 @@ def doubleml_sensitivity_manual(sensitivity_elements, all_coefs, psi, psi_deriv, cf_y, cf_d, rho, level): # specify the parameters - sigma2 = sensitivity_elements['sigma2'] - nu2 = sensitivity_elements['nu2'] - psi_sigma = sensitivity_elements['psi_sigma2'] - psi_nu = sensitivity_elements['psi_nu2'] + sigma2 = sensitivity_elements["sigma2"] + nu2 = sensitivity_elements["nu2"] + psi_sigma = sensitivity_elements["psi_sigma2"] + psi_nu = sensitivity_elements["psi_nu2"] psi_scaled = np.divide(psi, np.mean(psi_deriv, axis=0)) - confounding_strength = np.multiply(np.abs(rho), np.sqrt(np.multiply(cf_y, np.divide(cf_d, 1.0-cf_d)))) + confounding_strength = np.multiply(np.abs(rho), np.sqrt(np.multiply(cf_y, np.divide(cf_d, 1.0 - cf_d)))) S = np.sqrt(np.multiply(sigma2, nu2)) all_theta_lower = all_coefs - np.multiply(np.transpose(np.squeeze(S, axis=0)), confounding_strength) @@ -43,18 +43,13 @@ def doubleml_sensitivity_manual(sensitivity_elements, all_coefs, psi, psi_deriv, ci_lower = np.median(all_ci_lower, axis=1) ci_upper = np.median(all_ci_upper, axis=1) - theta_dict = {'lower': theta_lower, - 'upper': theta_upper} + theta_dict = {"lower": theta_lower, "upper": theta_upper} - se_dict = {'lower': sigma_lower, - 'upper': sigma_upper} + se_dict = {"lower": sigma_lower, "upper": sigma_upper} - ci_dict = {'lower': ci_lower, - 'upper': ci_upper} + ci_dict = {"lower": ci_lower, "upper": ci_upper} - res_dict = {'theta': theta_dict, - 'se': se_dict, - 'ci': ci_dict} + res_dict = {"theta": theta_dict, "se": se_dict, "ci": ci_dict} return res_dict @@ -68,10 +63,10 @@ def doubleml_sensitivity_benchmark_manual(dml_obj, benchmarking_set): dml_short.fit() var_y = np.var(dml_obj._dml_data.y) - var_y_long = np.squeeze(dml_obj.sensitivity_elements['sigma2'], axis=0) - nu2_long = np.squeeze(dml_obj.sensitivity_elements['nu2'], axis=0) - var_y_short = np.squeeze(dml_short.sensitivity_elements['sigma2'], axis=0) - nu2_short = np.squeeze(dml_short.sensitivity_elements['nu2'], axis=0) + var_y_long = np.squeeze(dml_obj.sensitivity_elements["sigma2"], axis=0) + nu2_long = np.squeeze(dml_obj.sensitivity_elements["nu2"], axis=0) + var_y_short = np.squeeze(dml_short.sensitivity_elements["sigma2"], axis=0) + nu2_short = np.squeeze(dml_short.sensitivity_elements["nu2"], axis=0) R2_y_long = 1.0 - var_y_long / var_y R2_y_short = 1.0 - var_y_short / var_y @@ -89,15 +84,15 @@ def doubleml_sensitivity_benchmark_manual(dml_obj, benchmarking_set): var_g = var_y_short - var_y_long var_riesz = nu2_long - nu2_short denom = np.sqrt(np.multiply(var_g, var_riesz), out=np.zeros_like(var_g), where=(var_g > 0) & (var_riesz > 0)) - all_rho_benchmark = np.sign(all_delta_theta) * \ - np.clip(np.divide(np.absolute(all_delta_theta), denom, out=np.ones_like(all_delta_theta), where=denom != 0), - 0, 1) + all_rho_benchmark = np.sign(all_delta_theta) * np.clip( + np.divide(np.absolute(all_delta_theta), denom, out=np.ones_like(all_delta_theta), where=denom != 0), 0, 1 + ) rho_benchmark = np.median(all_rho_benchmark, axis=0) benchmark_dict = { - 'cf_y': cf_y_benchmark, - 'cf_d': cf_d_benchmark, - 'rho': rho_benchmark, - 'delta_theta': delta_theta, + "cf_y": cf_y_benchmark, + "cf_d": cf_d_benchmark, + "rho": rho_benchmark, + "delta_theta": delta_theta, } return pd.DataFrame(benchmark_dict, index=dml_obj._dml_data.d_cols) diff --git a/doubleml/tests/conftest.py b/doubleml/tests/conftest.py index ffeceb3c7..248697b8f 100644 --- a/doubleml/tests/conftest.py +++ b/doubleml/tests/conftest.py @@ -11,7 +11,7 @@ def _g(x): return np.power(np.sin(x), 2) -def _m(x, nu=0., gamma=1.): +def _m(x, nu=0.0, gamma=1.0): return 0.5 / np.pi * (np.sinh(gamma)) / (np.cosh(gamma) - np.cos(x - nu)) @@ -19,8 +19,7 @@ def _m2(x): return np.power(x, 2) -@pytest.fixture(scope='session', - params=[(500, 5)]) +@pytest.fixture(scope="session", params=[(500, 5)]) def generate_data_simple(request): n_p = request.param np.random.seed(1111) @@ -34,18 +33,14 @@ def generate_data_simple(request): D2 = 1.0 * (np.random.uniform(size=n) > 0.5) X = np.random.normal(size=(n, p)) Y = theta * D1 + np.dot(X, np.ones(p)) + np.random.normal(size=n) - df = pd.DataFrame(np.column_stack((X, Y, D1, D2)), - columns=[f'X{i + 1}' for i in np.arange(p)] + ['Y', 'D1', 'D2']) - data_d1 = DoubleMLData(df, 'Y', 'D1') - data_d2 = DoubleMLData(df, 'Y', 'D2') + df = pd.DataFrame(np.column_stack((X, Y, D1, D2)), columns=[f"X{i + 1}" for i in np.arange(p)] + ["Y", "D1", "D2"]) + data_d1 = DoubleMLData(df, "Y", "D1") + data_d2 = DoubleMLData(df, "Y", "D2") return data_d1, data_d2 -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20), - (1000, 100)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20), (1000, 100)]) def generate_data1(request): n_p = request.param np.random.seed(1111) @@ -60,9 +55,7 @@ def generate_data1(request): return data -@pytest.fixture(scope='session', - params=[(500, 10), - (1000, 20)]) +@pytest.fixture(scope="session", params=[(500, 10), (1000, 20)]) def generate_data_irm_w_missings(request): n_p = request.param np.random.seed(1111) @@ -72,19 +65,17 @@ def generate_data_irm_w_missings(request): theta = 0.5 # generating data - (x, y, d) = make_irm_data(n, p, theta, return_type='array') + (x, y, d) = make_irm_data(n, p, theta, return_type="array") # randomly set some entries to np.nan - ind = np.random.choice(np.arange(x.size), replace=False, - size=int(x.size * 0.05)) + ind = np.random.choice(np.arange(x.size), replace=False, size=int(x.size * 0.05)) x[np.unravel_index(ind, x.shape)] = np.nan data = (x, y, d) return data -@pytest.fixture(scope='session', - params=[(1000, 20)]) +@pytest.fixture(scope="session", params=[(1000, 20)]) def generate_data_iv(request): n_p = request.param np.random.seed(1111) @@ -99,9 +90,7 @@ def generate_data_iv(request): return data -@pytest.fixture(scope='session', - params=[(253, 10, False), (501, 52, False), - (253, 10, True), (501, 52, True)]) +@pytest.fixture(scope="session", params=[(253, 10, False), (501, 52, False), (253, 10, True), (501, 52, True)]) def generate_data_cv_predict(request): np.random.seed(3141) # setting parameters @@ -120,8 +109,7 @@ def generate_data_cv_predict(request): return data -@pytest.fixture(scope='session', - params=[(1000, 20)]) +@pytest.fixture(scope="session", params=[(1000, 20)]) def generate_data_bivariate(request): n_p = request.param np.random.seed(1111) @@ -133,17 +121,38 @@ def generate_data_bivariate(request): sigma = make_spd_matrix(p) # generating data - x = np.random.multivariate_normal(np.zeros(p), sigma, size=[n, ]) + x = np.random.multivariate_normal( + np.zeros(p), + sigma, + size=[ + n, + ], + ) G = _g(np.dot(x, b)) M0 = _m(np.dot(x, b)) M1 = _m2(np.dot(x, b)) - D0 = M0 + np.random.standard_normal(size=[n, ]) - D1 = M1 + np.random.standard_normal(size=[n, ]) - y = theta[0] * D0 + theta[1] * D1 + G + np.random.standard_normal(size=[n, ]) + D0 = M0 + np.random.standard_normal( + size=[ + n, + ] + ) + D1 = M1 + np.random.standard_normal( + size=[ + n, + ] + ) + y = ( + theta[0] * D0 + + theta[1] * D1 + + G + + np.random.standard_normal( + size=[ + n, + ] + ) + ) d = np.column_stack((D0, D1)) - column_names = [f'X{i + 1}' for i in np.arange(p)] + ['y'] + \ - [f'd{i + 1}' for i in np.arange(2)] - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=column_names) + column_names = [f"X{i + 1}" for i in np.arange(p)] + ["y"] + [f"d{i + 1}" for i in np.arange(2)] + data = pd.DataFrame(np.column_stack((x, y, d)), columns=column_names) return data diff --git a/doubleml/tests/test_cv_predict.py b/doubleml/tests/test_cv_predict.py index 6aacc25d8..9887c88cf 100644 --- a/doubleml/tests/test_cv_predict.py +++ b/doubleml/tests/test_cv_predict.py @@ -8,32 +8,29 @@ from ._utils_dml_cv_predict import _dml_cv_predict_ut_version -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def cross_fit(request): return request.param -@pytest.fixture(scope='module', - params=[None, 'global', 'per_fold']) +@pytest.fixture(scope="module", params=[None, "global", "per_fold"]) def params(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def cv_predict_fixture(generate_data_cv_predict, cross_fit, params): n_folds = 4 # collect data (x, y, classifier) = generate_data_cv_predict if classifier: - method = 'predict_proba' + method = "predict_proba" else: - method = 'predict' + method = "predict" if cross_fit: - smpls = [(train, test) for train, test in KFold(n_splits=n_folds, - shuffle=True).split(x)] + smpls = [(train, test) for train, test in KFold(n_splits=n_folds, shuffle=True).split(x)] else: n_obs = len(y) smpls = train_test_split(np.arange(n_obs), test_size=0.23) @@ -41,44 +38,41 @@ def cv_predict_fixture(generate_data_cv_predict, cross_fit, params): if params is None: est_params = None - elif params == 'global': - if method == 'predict_proba': - est_params = {'C': 0.5} + elif params == "global": + if method == "predict_proba": + est_params = {"C": 0.5} else: - est_params = {'alpha': 0.5} + est_params = {"alpha": 0.5} else: - assert params == 'per_fold' - if method == 'predict_proba': + assert params == "per_fold" + if method == "predict_proba": if cross_fit: - est_params = [{'C': np.random.uniform()} for i in range(n_folds)] + est_params = [{"C": np.random.uniform()} for i in range(n_folds)] else: - est_params = {'C': 1.} + est_params = {"C": 1.0} else: if cross_fit: - est_params = [{'alpha': np.random.uniform()} for i in range(n_folds)] + est_params = [{"alpha": np.random.uniform()} for i in range(n_folds)] else: - est_params = {'alpha': 1.} + est_params = {"alpha": 1.0} - if method == 'predict_proba': - preds = _dml_cv_predict(LogisticRegression(), x, y, smpls, - est_params=est_params, method=method) - preds_ut = _dml_cv_predict_ut_version(LogisticRegression(), x, y, smpls, - est_params=est_params, method=method)[:, 1] + if method == "predict_proba": + preds = _dml_cv_predict(LogisticRegression(), x, y, smpls, est_params=est_params, method=method) + preds_ut = _dml_cv_predict_ut_version(LogisticRegression(), x, y, smpls, est_params=est_params, method=method)[:, 1] else: preds = _dml_cv_predict(Lasso(), x, y, smpls, est_params=est_params, method=method) preds_ut = _dml_cv_predict_ut_version(Lasso(), x, y, smpls, est_params=est_params, method=method) - res_dict = {'preds': preds['preds'], - 'preds_ut': preds_ut} + res_dict = {"preds": preds["preds"], "preds_ut": preds_ut} return res_dict @pytest.mark.ci def test_cv_predict(cv_predict_fixture): - ind_nan_preds = np.isnan(cv_predict_fixture['preds']) - ind_nan_preds_ut = np.isnan(cv_predict_fixture['preds_ut']) + ind_nan_preds = np.isnan(cv_predict_fixture["preds"]) + ind_nan_preds_ut = np.isnan(cv_predict_fixture["preds_ut"]) assert np.array_equal(ind_nan_preds, ind_nan_preds_ut) - assert np.allclose(cv_predict_fixture['preds'][~ind_nan_preds], - cv_predict_fixture['preds_ut'][~ind_nan_preds], - rtol=1e-9, atol=1e-4) + assert np.allclose( + cv_predict_fixture["preds"][~ind_nan_preds], cv_predict_fixture["preds_ut"][~ind_nan_preds], rtol=1e-9, atol=1e-4 + ) diff --git a/doubleml/tests/test_datasets.py b/doubleml/tests/test_datasets.py index 9cff04330..050e4f9af 100644 --- a/doubleml/tests/test_datasets.py +++ b/doubleml/tests/test_datasets.py @@ -21,38 +21,38 @@ make_ssm_data, ) -msg_inv_return_type = 'Invalid return_type.' +msg_inv_return_type = "Invalid return_type." def test_fetch_401K_return_types(): - res = fetch_401K('DoubleMLData') + res = fetch_401K("DoubleMLData") assert isinstance(res, DoubleMLData) - res = fetch_401K('DataFrame') + res = fetch_401K("DataFrame") assert isinstance(res, pd.DataFrame) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = fetch_401K('matrix') + _ = fetch_401K("matrix") def test_fetch_401K_poly(): - msg = 'polynomial_features os not implemented yet for fetch_401K.' + msg = "polynomial_features os not implemented yet for fetch_401K." with pytest.raises(NotImplementedError, match=msg): _ = fetch_401K(polynomial_features=True) def test_fetch_bonus_return_types(): - res = fetch_bonus('DoubleMLData') + res = fetch_bonus("DoubleMLData") assert isinstance(res, DoubleMLData) - res = fetch_bonus('DataFrame') + res = fetch_bonus("DataFrame") assert isinstance(res, pd.DataFrame) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = fetch_bonus('matrix') + _ = fetch_bonus("matrix") def test_fetch_bonus_poly(): data_bonus_wo_poly = fetch_bonus(polynomial_features=False) n_x = len(data_bonus_wo_poly.x_cols) data_bonus_w_poly = fetch_bonus(polynomial_features=True) - assert len(data_bonus_w_poly.x_cols) == ((n_x+1) * n_x / 2 + n_x) + assert len(data_bonus_w_poly.x_cols) == ((n_x + 1) * n_x / 2 + n_x) @pytest.mark.ci @@ -67,112 +67,110 @@ def test_make_plr_CCDDHNR2018_return_types(): assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_plr_CCDDHNR2018(n_obs=100, return_type='matrix') + _ = make_plr_CCDDHNR2018(n_obs=100, return_type="matrix") @pytest.mark.ci def test_make_plr_turrell2018_return_types(): np.random.seed(3141) - res = make_plr_turrell2018(n_obs=100, return_type='DoubleMLData') + res = make_plr_turrell2018(n_obs=100, return_type="DoubleMLData") assert isinstance(res, DoubleMLData) - res = make_plr_turrell2018(n_obs=100, return_type='DataFrame') + res = make_plr_turrell2018(n_obs=100, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d = make_plr_turrell2018(n_obs=100, return_type='array') + x, y, d = make_plr_turrell2018(n_obs=100, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_plr_turrell2018(n_obs=100, return_type='matrix') + _ = make_plr_turrell2018(n_obs=100, return_type="matrix") @pytest.mark.ci def test_make_irm_data_return_types(): np.random.seed(3141) - res = make_irm_data(n_obs=100, return_type='DoubleMLData') + res = make_irm_data(n_obs=100, return_type="DoubleMLData") assert isinstance(res, DoubleMLData) - res = make_irm_data(n_obs=100, return_type='DataFrame') + res = make_irm_data(n_obs=100, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d = make_irm_data(n_obs=100, return_type='array') + x, y, d = make_irm_data(n_obs=100, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_irm_data(n_obs=100, return_type='matrix') + _ = make_irm_data(n_obs=100, return_type="matrix") @pytest.mark.ci def test_make_iivm_data_return_types(): np.random.seed(3141) - res = make_iivm_data(n_obs=100, return_type='DoubleMLData') + res = make_iivm_data(n_obs=100, return_type="DoubleMLData") assert isinstance(res, DoubleMLData) - res = make_iivm_data(n_obs=100, return_type='DataFrame') + res = make_iivm_data(n_obs=100, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d, z = make_iivm_data(n_obs=100, return_type='array') + x, y, d, z = make_iivm_data(n_obs=100, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) assert isinstance(z, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_iivm_data(n_obs=100, return_type='matrix') + _ = make_iivm_data(n_obs=100, return_type="matrix") @pytest.mark.ci def test_make_pliv_data_return_types(): np.random.seed(3141) - res = _make_pliv_data(n_obs=100, return_type='DoubleMLData') + res = _make_pliv_data(n_obs=100, return_type="DoubleMLData") assert isinstance(res, DoubleMLData) - res = _make_pliv_data(n_obs=100, return_type='DataFrame') + res = _make_pliv_data(n_obs=100, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d, z = _make_pliv_data(n_obs=100, return_type='array') + x, y, d, z = _make_pliv_data(n_obs=100, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) assert isinstance(z, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = _make_pliv_data(n_obs=100, return_type='matrix') + _ = _make_pliv_data(n_obs=100, return_type="matrix") @pytest.mark.ci def test_make_pliv_CHS2015_return_types(): np.random.seed(3141) - res = make_pliv_CHS2015(n_obs=100, return_type='DoubleMLData') + res = make_pliv_CHS2015(n_obs=100, return_type="DoubleMLData") assert isinstance(res, DoubleMLData) - res = make_pliv_CHS2015(n_obs=100, return_type='DataFrame') + res = make_pliv_CHS2015(n_obs=100, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d, z = make_pliv_CHS2015(n_obs=100, return_type='array') + x, y, d, z = make_pliv_CHS2015(n_obs=100, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) assert isinstance(z, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_pliv_CHS2015(n_obs=100, return_type='matrix') + _ = make_pliv_CHS2015(n_obs=100, return_type="matrix") @pytest.mark.ci def test_make_pliv_multiway_cluster_CKMS2021_return_types(): np.random.seed(3141) - res = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type='DoubleMLClusterData') + res = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type="DoubleMLClusterData") assert isinstance(res, DoubleMLClusterData) - res = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type='DataFrame') + res = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d, cluster_vars, z = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type='array') + x, y, d, cluster_vars, z = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) assert isinstance(cluster_vars, np.ndarray) assert isinstance(z, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type='matrix') + _ = make_pliv_multiway_cluster_CKMS2021(N=10, M=10, return_type="matrix") -@pytest.fixture(scope='function', - params=[False, True]) +@pytest.fixture(scope="function", params=[False, True]) def cross_sectional(request): return request.param -@pytest.fixture(scope='function', - params=[1, 2, 3, 4, 5, 6]) +@pytest.fixture(scope="function", params=[1, 2, 3, 4, 5, 6]) def dgp_type(request): return request.param @@ -185,8 +183,9 @@ def test_make_did_SZ2020_return_types(cross_sectional, dgp_type): res = make_did_SZ2020(n_obs=100, dgp_type=dgp_type, cross_sectional_data=cross_sectional, return_type=pd.DataFrame) assert isinstance(res, pd.DataFrame) if cross_sectional: - x, y, d, t = make_did_SZ2020(n_obs=100, dgp_type=dgp_type, cross_sectional_data=cross_sectional, - return_type=np.ndarray) + x, y, d, t = make_did_SZ2020( + n_obs=100, dgp_type=dgp_type, cross_sectional_data=cross_sectional, return_type=np.ndarray + ) assert isinstance(t, np.ndarray) else: x, y, d = make_did_SZ2020(n_obs=100, dgp_type=dgp_type, cross_sectional_data=cross_sectional, return_type=np.ndarray) @@ -194,14 +193,13 @@ def test_make_did_SZ2020_return_types(cross_sectional, dgp_type): assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_did_SZ2020(n_obs=100, dgp_type=dgp_type, cross_sectional_data=cross_sectional, return_type='matrix') - msg = 'The dgp_type is not valid.' + _ = make_did_SZ2020(n_obs=100, dgp_type=dgp_type, cross_sectional_data=cross_sectional, return_type="matrix") + msg = "The dgp_type is not valid." with pytest.raises(ValueError, match=msg): - _ = make_did_SZ2020(n_obs=100, dgp_type="5", cross_sectional_data=cross_sectional, return_type='matrix') + _ = make_did_SZ2020(n_obs=100, dgp_type="5", cross_sectional_data=cross_sectional, return_type="matrix") -@pytest.fixture(scope='function', - params=[True, False]) +@pytest.fixture(scope="function", params=[True, False]) def linear(request): return request.param @@ -211,26 +209,26 @@ def test_make_confounded_irm_data_return_types(linear): np.random.seed(3141) res = make_confounded_irm_data(linear=linear) assert isinstance(res, dict) - assert isinstance(res['x'], np.ndarray) - assert isinstance(res['y'], np.ndarray) - assert isinstance(res['d'], np.ndarray) - - assert isinstance(res['oracle_values'], dict) - assert isinstance(res['oracle_values']['g_long'], np.ndarray) - assert isinstance(res['oracle_values']['g_short'], np.ndarray) - assert isinstance(res['oracle_values']['m_long'], np.ndarray) - assert isinstance(res['oracle_values']['m_short'], np.ndarray) - assert isinstance(res['oracle_values']['gamma_a'], float) - assert isinstance(res['oracle_values']['beta_a'], float) - assert isinstance(res['oracle_values']['a'], np.ndarray) - assert isinstance(res['oracle_values']['y_0'], np.ndarray) - assert isinstance(res['oracle_values']['y_1'], np.ndarray) - assert isinstance(res['oracle_values']['z'], np.ndarray) - assert isinstance(res['oracle_values']['cf_y'], float) - assert isinstance(res['oracle_values']['cf_d_ate'], float) - assert isinstance(res['oracle_values']['cf_d_atte'], float) - assert isinstance(res['oracle_values']['rho_ate'], float) - assert isinstance(res['oracle_values']['rho_atte'], float) + assert isinstance(res["x"], np.ndarray) + assert isinstance(res["y"], np.ndarray) + assert isinstance(res["d"], np.ndarray) + + assert isinstance(res["oracle_values"], dict) + assert isinstance(res["oracle_values"]["g_long"], np.ndarray) + assert isinstance(res["oracle_values"]["g_short"], np.ndarray) + assert isinstance(res["oracle_values"]["m_long"], np.ndarray) + assert isinstance(res["oracle_values"]["m_short"], np.ndarray) + assert isinstance(res["oracle_values"]["gamma_a"], float) + assert isinstance(res["oracle_values"]["beta_a"], float) + assert isinstance(res["oracle_values"]["a"], np.ndarray) + assert isinstance(res["oracle_values"]["y_0"], np.ndarray) + assert isinstance(res["oracle_values"]["y_1"], np.ndarray) + assert isinstance(res["oracle_values"]["z"], np.ndarray) + assert isinstance(res["oracle_values"]["cf_y"], float) + assert isinstance(res["oracle_values"]["cf_d_ate"], float) + assert isinstance(res["oracle_values"]["cf_d_atte"], float) + assert isinstance(res["oracle_values"]["rho_ate"], float) + assert isinstance(res["oracle_values"]["rho_atte"], float) @pytest.mark.ci @@ -238,30 +236,28 @@ def test_make_confounded_plr_data_return_types(): np.random.seed(3141) res = make_confounded_plr_data(theta=5.0) assert isinstance(res, dict) - assert isinstance(res['x'], np.ndarray) - assert isinstance(res['y'], np.ndarray) - assert isinstance(res['d'], np.ndarray) - - assert isinstance(res['oracle_values'], dict) - assert isinstance(res['oracle_values']['g_long'], np.ndarray) - assert isinstance(res['oracle_values']['g_short'], np.ndarray) - assert isinstance(res['oracle_values']['m_long'], np.ndarray) - assert isinstance(res['oracle_values']['m_short'], np.ndarray) - assert isinstance(res['oracle_values']['theta'], float) - assert isinstance(res['oracle_values']['gamma_a'], float) - assert isinstance(res['oracle_values']['beta_a'], float) - assert isinstance(res['oracle_values']['a'], np.ndarray) - assert isinstance(res['oracle_values']['z'], np.ndarray) - - -@pytest.fixture(scope='function', - params=[False, True]) + assert isinstance(res["x"], np.ndarray) + assert isinstance(res["y"], np.ndarray) + assert isinstance(res["d"], np.ndarray) + + assert isinstance(res["oracle_values"], dict) + assert isinstance(res["oracle_values"]["g_long"], np.ndarray) + assert isinstance(res["oracle_values"]["g_short"], np.ndarray) + assert isinstance(res["oracle_values"]["m_long"], np.ndarray) + assert isinstance(res["oracle_values"]["m_short"], np.ndarray) + assert isinstance(res["oracle_values"]["theta"], float) + assert isinstance(res["oracle_values"]["gamma_a"], float) + assert isinstance(res["oracle_values"]["beta_a"], float) + assert isinstance(res["oracle_values"]["a"], np.ndarray) + assert isinstance(res["oracle_values"]["z"], np.ndarray) + + +@pytest.fixture(scope="function", params=[False, True]) def binary_treatment(request): return request.param -@pytest.fixture(scope='function', - params=[1, 2]) +@pytest.fixture(scope="function", params=[1, 2]) def n_x(request): return request.param @@ -271,18 +267,18 @@ def test_make_heterogeneous_data_return_types(binary_treatment, n_x): np.random.seed(3141) res = make_heterogeneous_data(n_obs=100, n_x=n_x, binary_treatment=binary_treatment) assert isinstance(res, dict) - assert isinstance(res['data'], pd.DataFrame) - assert isinstance(res['effects'], np.ndarray) - assert callable(res['treatment_effect']) + assert isinstance(res["data"], pd.DataFrame) + assert isinstance(res["effects"], np.ndarray) + assert callable(res["treatment_effect"]) # test input checks - msg = 'n_x must be either 1 or 2.' + msg = "n_x must be either 1 or 2." with pytest.raises(AssertionError, match=msg): _ = make_heterogeneous_data(n_obs=100, n_x=0, binary_treatment=binary_treatment) - msg = 'support_size must be smaller than p.' + msg = "support_size must be smaller than p." with pytest.raises(AssertionError, match=msg): _ = make_heterogeneous_data(n_obs=100, n_x=n_x, support_size=31, binary_treatment=binary_treatment) - msg = 'binary_treatment must be a boolean.' + msg = "binary_treatment must be a boolean." with pytest.raises(AssertionError, match=msg): _ = make_heterogeneous_data(n_obs=100, n_x=n_x, binary_treatment=2) @@ -292,20 +288,19 @@ def test_make_ssm_data_return_types(): np.random.seed(3141) res = make_ssm_data(n_obs=100) assert isinstance(res, DoubleMLData) - res = make_ssm_data(n_obs=100, return_type='DataFrame') + res = make_ssm_data(n_obs=100, return_type="DataFrame") assert isinstance(res, pd.DataFrame) - x, y, d, z, s = make_ssm_data(n_obs=100, return_type='array') + x, y, d, z, s = make_ssm_data(n_obs=100, return_type="array") assert isinstance(x, np.ndarray) assert isinstance(y, np.ndarray) assert isinstance(d, np.ndarray) assert isinstance(z, np.ndarray) assert isinstance(s, np.ndarray) with pytest.raises(ValueError, match=msg_inv_return_type): - _ = make_ssm_data(n_obs=100, return_type='matrix') + _ = make_ssm_data(n_obs=100, return_type="matrix") -@pytest.fixture(scope='function', - params=[3, 5]) +@pytest.fixture(scope="function", params=[3, 5]) def n_levels(request): return request.param @@ -315,21 +310,21 @@ def test_make_data_discrete_treatments(n_levels): n = 100 data_apo = make_irm_data_discrete_treatments(n_obs=n, n_levels=3) assert isinstance(data_apo, dict) - assert isinstance(data_apo['y'], np.ndarray) - assert isinstance(data_apo['d'], np.ndarray) - assert isinstance(data_apo['x'], np.ndarray) - assert isinstance(data_apo['oracle_values'], dict) - - assert isinstance(data_apo['oracle_values']['cont_d'], np.ndarray) - assert isinstance(data_apo['oracle_values']['level_bounds'], np.ndarray) - assert isinstance(data_apo['oracle_values']['potential_level'], np.ndarray) - assert isinstance(data_apo['oracle_values']['ite'], np.ndarray) - assert isinstance(data_apo['oracle_values']['y0'], np.ndarray) - - msg = 'n_levels must be at least 2.' + assert isinstance(data_apo["y"], np.ndarray) + assert isinstance(data_apo["d"], np.ndarray) + assert isinstance(data_apo["x"], np.ndarray) + assert isinstance(data_apo["oracle_values"], dict) + + assert isinstance(data_apo["oracle_values"]["cont_d"], np.ndarray) + assert isinstance(data_apo["oracle_values"]["level_bounds"], np.ndarray) + assert isinstance(data_apo["oracle_values"]["potential_level"], np.ndarray) + assert isinstance(data_apo["oracle_values"]["ite"], np.ndarray) + assert isinstance(data_apo["oracle_values"]["y0"], np.ndarray) + + msg = "n_levels must be at least 2." with pytest.raises(ValueError, match=msg): _ = make_irm_data_discrete_treatments(n_obs=n, n_levels=1) - msg = 'n_levels must be an integer.' + msg = "n_levels must be an integer." with pytest.raises(ValueError, match=msg): _ = make_irm_data_discrete_treatments(n_obs=n, n_levels=1.1) diff --git a/doubleml/tests/test_dml_data.py b/doubleml/tests/test_dml_data.py index 453b42231..fbb92f33c 100644 --- a/doubleml/tests/test_dml_data.py +++ b/doubleml/tests/test_dml_data.py @@ -27,7 +27,7 @@ def n_coefs(self): @pytest.mark.ci def test_doubleml_basedata(): dummy_dml_data = DummyDataClass(pd.DataFrame(np.zeros((100, 10)))) - assert dummy_dml_data.d_cols[0] == 'theta' + assert dummy_dml_data.d_cols[0] == "theta" assert dummy_dml_data.n_treat == 1 assert dummy_dml_data.n_coefs == 1 @@ -36,116 +36,117 @@ def test_doubleml_basedata(): def dml_data_fixture(generate_data1): data = generate_data1 np.random.seed(3141) - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() - obj_from_np = DoubleMLData.from_arrays(data.loc[:, x_cols].values, - data['y'].values, data['d'].values) + obj_from_np = DoubleMLData.from_arrays(data.loc[:, x_cols].values, data["y"].values, data["d"].values) - obj_from_pd = DoubleMLData(data, 'y', ['d'], x_cols) + obj_from_pd = DoubleMLData(data, "y", ["d"], x_cols) - return {'obj_from_np': obj_from_np, - 'obj_from_pd': obj_from_pd} + return {"obj_from_np": obj_from_np, "obj_from_pd": obj_from_pd} @pytest.mark.ci def test_dml_data_x(dml_data_fixture): - assert np.allclose(dml_data_fixture['obj_from_np'].x, - dml_data_fixture['obj_from_pd'].x, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_data_fixture["obj_from_np"].x, dml_data_fixture["obj_from_pd"].x, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_data_y(dml_data_fixture): - assert np.allclose(dml_data_fixture['obj_from_np'].y, - dml_data_fixture['obj_from_pd'].y, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_data_fixture["obj_from_np"].y, dml_data_fixture["obj_from_pd"].y, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_data_d(dml_data_fixture): - assert np.allclose(dml_data_fixture['obj_from_np'].d, - dml_data_fixture['obj_from_pd'].d, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_data_fixture["obj_from_np"].d, dml_data_fixture["obj_from_pd"].d, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_obj_vs_from_arrays(): np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=100) - dml_data_from_array = DoubleMLData.from_arrays(dml_data.data[dml_data.x_cols], - dml_data.data[dml_data.y_col], - dml_data.data[dml_data.d_cols]) + dml_data_from_array = DoubleMLData.from_arrays( + dml_data.data[dml_data.x_cols], dml_data.data[dml_data.y_col], dml_data.data[dml_data.d_cols] + ) assert dml_data_from_array.data.equals(dml_data.data) dml_data = _make_pliv_data(n_obs=100) - dml_data_from_array = DoubleMLData.from_arrays(dml_data.data[dml_data.x_cols], - dml_data.data[dml_data.y_col], - dml_data.data[dml_data.d_cols], - dml_data.data[dml_data.z_cols]) + dml_data_from_array = DoubleMLData.from_arrays( + dml_data.data[dml_data.x_cols], + dml_data.data[dml_data.y_col], + dml_data.data[dml_data.d_cols], + dml_data.data[dml_data.z_cols], + ) assert dml_data_from_array.data.equals(dml_data.data) dml_data = make_pliv_CHS2015(n_obs=100, dim_z=5) - dml_data_from_array = DoubleMLData.from_arrays(dml_data.data[dml_data.x_cols], - dml_data.data[dml_data.y_col], - dml_data.data[dml_data.d_cols], - dml_data.data[dml_data.z_cols]) + dml_data_from_array = DoubleMLData.from_arrays( + dml_data.data[dml_data.x_cols], + dml_data.data[dml_data.y_col], + dml_data.data[dml_data.d_cols], + dml_data.data[dml_data.z_cols], + ) assert np.array_equal(dml_data_from_array.data, dml_data.data) # z_cols name differ dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f'X{i+1}' for i in np.arange(7)] + ['y', 'd1', 'd2'] - dml_data = DoubleMLData(df, 'y', ['d1', 'd2'], [f'X{i+1}' for i in np.arange(7)]) - dml_data_from_array = DoubleMLData.from_arrays(dml_data.data[dml_data.x_cols], - dml_data.data[dml_data.y_col], - dml_data.data[dml_data.d_cols]) + df.columns = [f"X{i+1}" for i in np.arange(7)] + ["y", "d1", "d2"] + dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i+1}" for i in np.arange(7)]) + dml_data_from_array = DoubleMLData.from_arrays( + dml_data.data[dml_data.x_cols], dml_data.data[dml_data.y_col], dml_data.data[dml_data.d_cols] + ) assert np.array_equal(dml_data_from_array.data, dml_data.data) dml_data = make_did_SZ2020(n_obs=100, cross_sectional_data=False) - dml_data_from_array = DoubleMLData.from_arrays(x=dml_data.data[dml_data.x_cols], - y=dml_data.data[dml_data.y_col], - d=dml_data.data[dml_data.d_cols]) + dml_data_from_array = DoubleMLData.from_arrays( + x=dml_data.data[dml_data.x_cols], y=dml_data.data[dml_data.y_col], d=dml_data.data[dml_data.d_cols] + ) assert np.array_equal(dml_data_from_array.data, dml_data.data) dml_data = make_did_SZ2020(n_obs=100, cross_sectional_data=True) - dml_data_from_array = DoubleMLData.from_arrays(x=dml_data.data[dml_data.x_cols], - y=dml_data.data[dml_data.y_col], - d=dml_data.data[dml_data.d_cols], - t=dml_data.data[dml_data.t_col]) + dml_data_from_array = DoubleMLData.from_arrays( + x=dml_data.data[dml_data.x_cols], + y=dml_data.data[dml_data.y_col], + d=dml_data.data[dml_data.d_cols], + t=dml_data.data[dml_data.t_col], + ) assert np.array_equal(dml_data_from_array.data, dml_data.data) # check with instrument and time variable dml_data = make_did_SZ2020(n_obs=100, cross_sectional_data=True) - dml_data.data['z'] = dml_data.data['t'] - dml_data_from_array = DoubleMLData.from_arrays(x=dml_data.data[dml_data.x_cols], - y=dml_data.data[dml_data.y_col], - d=dml_data.data[dml_data.d_cols], - z=dml_data.data['z'], - t=dml_data.data[dml_data.t_col]) + dml_data.data["z"] = dml_data.data["t"] + dml_data_from_array = DoubleMLData.from_arrays( + x=dml_data.data[dml_data.x_cols], + y=dml_data.data[dml_data.y_col], + d=dml_data.data[dml_data.d_cols], + z=dml_data.data["z"], + t=dml_data.data[dml_data.t_col], + ) assert np.array_equal(dml_data_from_array.data, dml_data.data) dml_data = make_pliv_multiway_cluster_CKMS2021(N=10, M=10) - dml_data_from_array = DoubleMLClusterData.from_arrays(dml_data.data[dml_data.x_cols], - dml_data.data[dml_data.y_col], - dml_data.data[dml_data.d_cols], - dml_data.data[dml_data.cluster_cols], - dml_data.data[dml_data.z_cols]) + dml_data_from_array = DoubleMLClusterData.from_arrays( + dml_data.data[dml_data.x_cols], + dml_data.data[dml_data.y_col], + dml_data.data[dml_data.d_cols], + dml_data.data[dml_data.cluster_cols], + dml_data.data[dml_data.z_cols], + ) df = dml_data.data.copy() - df.rename(columns={'cluster_var_i': 'cluster_var1', - 'cluster_var_j': 'cluster_var2', - 'Y': 'y', 'D': 'd', 'Z': 'z'}, - inplace=True) + df.rename( + columns={"cluster_var_i": "cluster_var1", "cluster_var_j": "cluster_var2", "Y": "y", "D": "d", "Z": "z"}, inplace=True + ) assert dml_data_from_array.data.equals(df) # with a single cluster variable - dml_data_from_array = DoubleMLClusterData.from_arrays(dml_data.data[dml_data.x_cols], - dml_data.data[dml_data.y_col], - dml_data.data[dml_data.d_cols], - dml_data.data[dml_data.cluster_cols[1]], - dml_data.data[dml_data.z_cols]) - df = dml_data.data.copy().drop(columns='cluster_var_i') - df.rename(columns={'cluster_var_j': 'cluster_var', - 'Y': 'y', 'D': 'd', 'Z': 'z'}, - inplace=True) + dml_data_from_array = DoubleMLClusterData.from_arrays( + dml_data.data[dml_data.x_cols], + dml_data.data[dml_data.y_col], + dml_data.data[dml_data.d_cols], + dml_data.data[dml_data.cluster_cols[1]], + dml_data.data[dml_data.z_cols], + ) + df = dml_data.data.copy().drop(columns="cluster_var_i") + df.rename(columns={"cluster_var_j": "cluster_var", "Y": "y", "D": "d", "Z": "z"}, inplace=True) assert dml_data_from_array.data.equals(df) @@ -153,9 +154,9 @@ def test_obj_vs_from_arrays(): def test_add_vars_in_df(): # additional variables in the df shouldn't affect results np.random.seed(3141) - df = make_plr_CCDDHNR2018(n_obs=100, return_type='DataFrame') - dml_data_full_df = DoubleMLData(df, 'y', 'd', ['X1', 'X11', 'X13']) - dml_data_subset = DoubleMLData(df[['X1', 'X11', 'X13'] + ['y', 'd']], 'y', 'd', ['X1', 'X11', 'X13']) + df = make_plr_CCDDHNR2018(n_obs=100, return_type="DataFrame") + dml_data_full_df = DoubleMLData(df, "y", "d", ["X1", "X11", "X13"]) + dml_data_subset = DoubleMLData(df[["X1", "X11", "X13"] + ["y", "d"]], "y", "d", ["X1", "X11", "X13"]) dml_plr_full_df = DoubleMLPLR(dml_data_full_df, Lasso(), Lasso()) dml_plr_subset = DoubleMLPLR(dml_data_subset, Lasso(), Lasso(), draw_sample_splitting=False) dml_plr_subset.set_sample_splitting(dml_plr_full_df.smpls) @@ -173,7 +174,7 @@ def test_dml_data_no_instr_no_time_no_selection(): assert dml_data.n_instr == 0 assert dml_data.t is None - x, y, d = make_plr_CCDDHNR2018(n_obs=100, return_type='array') + x, y, d = make_plr_CCDDHNR2018(n_obs=100, return_type="array") dml_data = DoubleMLData.from_arrays(x, y, d) assert dml_data.z is None assert dml_data.n_instr == 0 @@ -207,110 +208,90 @@ def test_dml_summary_with_selection(): @pytest.mark.ci def test_x_cols_setter_defaults(): - df = pd.DataFrame(np.tile(np.arange(4), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(4), (4, 1)), columns=["yy", "dd", "xx1", "xx2"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument - df = pd.DataFrame(np.tile(np.arange(5), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', z_cols='zz') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(5), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "zz"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", z_cols="zz") + assert dml_data.x_cols == ["xx1", "xx2"] # without instrument with time - df = pd.DataFrame(np.tile(np.arange(5), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'tt']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', t_col='tt') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(5), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "tt"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", t_col="tt") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument with time - df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz', 'tt']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', z_cols='zz', t_col='tt') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "zz", "tt"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", z_cols="zz", t_col="tt") + assert dml_data.x_cols == ["xx1", "xx2"] # without instrument with selection - df = pd.DataFrame(np.tile(np.arange(5), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'ss']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(5), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "ss"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument with selection - df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz', 'ss']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', z_cols='zz', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "zz", "ss"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", z_cols="zz", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] # with selection and time - df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'tt', 'ss']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', t_col='tt', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "tt", "ss"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", t_col="tt", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument, selection and time - df = pd.DataFrame(np.tile(np.arange(7), (4, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz', 'tt', 'ss']) - dml_data = DoubleMLData(df, y_col='yy', d_cols='dd', z_cols='zz', t_col='tt', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(7), (4, 1)), columns=["yy", "dd", "xx1", "xx2", "zz", "tt", "ss"]) + dml_data = DoubleMLData(df, y_col="yy", d_cols="dd", z_cols="zz", t_col="tt", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] @pytest.mark.ci def test_x_cols_setter_defaults_w_cluster(): - df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'xx3', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1') - assert dml_data.x_cols == ['xx1', 'xx2', 'xx3'] - dml_data.x_cols = ['xx1', 'xx3'] - assert dml_data.x_cols == ['xx1', 'xx3'] + df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "xx3", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1") + assert dml_data.x_cols == ["xx1", "xx2", "xx3"] + dml_data.x_cols = ["xx1", "xx3"] + assert dml_data.x_cols == ["xx1", "xx3"] dml_data.x_cols = None - assert dml_data.x_cols == ['xx1', 'xx2', 'xx3'] + assert dml_data.x_cols == ["xx1", "xx2", "xx3"] # with instrument - df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'z', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', z_cols='z') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "z", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", z_cols="z") + assert dml_data.x_cols == ["xx1", "xx2"] # without instrument and with time - df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'tt', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', t_col='tt') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "tt", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", t_col="tt") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument and with time - df = pd.DataFrame(np.tile(np.arange(7), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz', 'tt', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', - z_cols='zz', t_col='tt') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(7), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "zz", "tt", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", z_cols="zz", t_col="tt") + assert dml_data.x_cols == ["xx1", "xx2"] # without instrument and with selection - df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'ss', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(6), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "ss", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument and with selection - df = pd.DataFrame(np.tile(np.arange(7), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz', 'ss', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', - z_cols='zz', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(7), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "zz", "ss", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", z_cols="zz", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] # without instrument with time with selection - df = pd.DataFrame(np.tile(np.arange(7), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'tt', 'ss', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', t_col='tt', - s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(7), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "tt", "ss", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", t_col="tt", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] # with instrument with time with selection - df = pd.DataFrame(np.tile(np.arange(8), (6, 1)), - columns=['yy', 'dd', 'xx1', 'xx2', 'zz', 'tt', 'ss', 'cluster1']) - dml_data = DoubleMLClusterData(df, y_col='yy', d_cols='dd', cluster_cols='cluster1', - z_cols='zz', t_col='tt', s_col='ss') - assert dml_data.x_cols == ['xx1', 'xx2'] + df = pd.DataFrame(np.tile(np.arange(8), (6, 1)), columns=["yy", "dd", "xx1", "xx2", "zz", "tt", "ss", "cluster1"]) + dml_data = DoubleMLClusterData(df, y_col="yy", d_cols="dd", cluster_cols="cluster1", z_cols="zz", t_col="tt", s_col="ss") + assert dml_data.x_cols == ["xx1", "xx2"] @pytest.mark.ci @@ -320,22 +301,21 @@ def test_x_cols_setter(): orig_x_cols = dml_data.x_cols # check that after changing the x_cols, the x array gets updated - x_comp = dml_data.data[['X1', 'X11', 'X13']].values - dml_data.x_cols = ['X1', 'X11', 'X13'] + x_comp = dml_data.data[["X1", "X11", "X13"]].values + dml_data.x_cols = ["X1", "X11", "X13"] assert np.array_equal(dml_data.x, x_comp) - msg = 'Invalid covariates x_cols. At least one covariate is no data column.' + msg = "Invalid covariates x_cols. At least one covariate is no data column." with pytest.raises(ValueError, match=msg): - dml_data.x_cols = ['X1', 'X11', 'A13'] + dml_data.x_cols = ["X1", "X11", "A13"] - msg = (r'The covariates x_cols must be of str or list type \(or None\). ' - "5 of type was passed.") + msg = r"The covariates x_cols must be of str or list type \(or None\). " "5 of type was passed." with pytest.raises(TypeError, match=msg): dml_data.x_cols = 5 # check single covariate - x_comp = dml_data.data[['X13']].values - dml_data.x_cols = 'X13' + x_comp = dml_data.data[["X13"]].values + dml_data.x_cols = "X13" assert np.array_equal(dml_data.x, x_comp) # check setting None brings us back to orig_x_cols @@ -349,28 +329,27 @@ def test_d_cols_setter(): np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f'X{i + 1}' for i in np.arange(7)] + ['y', 'd1', 'd2'] - dml_data = DoubleMLData(df, 'y', ['d1', 'd2'], [f'X{i + 1}' for i in np.arange(7)]) + df.columns = [f"X{i + 1}" for i in np.arange(7)] + ["y", "d1", "d2"] + dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i + 1}" for i in np.arange(7)]) # check that after changing d_cols, the d array gets updated - d_comp = dml_data.data['d2'].values - dml_data.d_cols = ['d2', 'd1'] + d_comp = dml_data.data["d2"].values + dml_data.d_cols = ["d2", "d1"] assert np.array_equal(dml_data.d, d_comp) - msg = r'Invalid treatment variable\(s\) d_cols. At least one treatment variable is no data column.' + msg = r"Invalid treatment variable\(s\) d_cols. At least one treatment variable is no data column." with pytest.raises(ValueError, match=msg): - dml_data.d_cols = ['d1', 'd13'] + dml_data.d_cols = ["d1", "d13"] with pytest.raises(ValueError, match=msg): - dml_data.d_cols = 'd13' + dml_data.d_cols = "d13" - msg = (r'The treatment variable\(s\) d_cols must be of str or list type. ' - "5 of type was passed.") + msg = r"The treatment variable\(s\) d_cols must be of str or list type. " "5 of type was passed." with pytest.raises(TypeError, match=msg): dml_data.d_cols = 5 # check single treatment variable - d_comp = dml_data.data['d2'].values - dml_data.d_cols = 'd2' + d_comp = dml_data.data["d2"].values + dml_data.d_cols = "d2" assert np.array_equal(dml_data.d, d_comp) assert dml_data.n_treat == 1 @@ -380,30 +359,30 @@ def test_z_cols_setter(): np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f'X{i + 1}' for i in np.arange(4)] + [f'z{i + 1}' for i in np.arange(3)] + ['y', 'd1', 'd2'] - dml_data = DoubleMLData(df, 'y', ['d1', 'd2'], - [f'X{i + 1}' for i in np.arange(4)], - [f'z{i + 1}' for i in np.arange(3)]) + df.columns = [f"X{i + 1}" for i in np.arange(4)] + [f"z{i + 1}" for i in np.arange(3)] + ["y", "d1", "d2"] + dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i + 1}" for i in np.arange(4)], [f"z{i + 1}" for i in np.arange(3)]) # check that after changing z_cols, the z array gets updated - z_comp = dml_data.data[['z1', 'z2']].values - dml_data.z_cols = ['z1', 'z2'] + z_comp = dml_data.data[["z1", "z2"]].values + dml_data.z_cols = ["z1", "z2"] assert np.array_equal(dml_data.z, z_comp) - msg = r'Invalid instrumental variable\(s\) z_cols. At least one instrumental variable is no data column.' + msg = r"Invalid instrumental variable\(s\) z_cols. At least one instrumental variable is no data column." with pytest.raises(ValueError, match=msg): - dml_data.z_cols = ['z1', 'a13'] + dml_data.z_cols = ["z1", "a13"] with pytest.raises(ValueError, match=msg): - dml_data.z_cols = 'a13' + dml_data.z_cols = "a13" - msg = (r'The instrumental variable\(s\) z_cols must be of str or list type \(or None\). ' - "5 of type was passed.") + msg = ( + r"The instrumental variable\(s\) z_cols must be of str or list type \(or None\). " + "5 of type was passed." + ) with pytest.raises(TypeError, match=msg): dml_data.z_cols = 5 # check single instrument - z_comp = dml_data.data[['z2']].values - dml_data.z_cols = 'z2' + z_comp = dml_data.data[["z2"]].values + dml_data.z_cols = "z2" assert np.array_equal(dml_data.z, z_comp) # check None @@ -416,22 +395,19 @@ def test_z_cols_setter(): def test_t_col_setter(): np.random.seed(3141) df = make_did_SZ2020(n_obs=100, cross_sectional_data=True, return_type=pd.DataFrame) - df['t_new'] = np.ones(shape=(100,)) - dml_data = DoubleMLData(df, 'y', 'd', - [f'Z{i + 1}' for i in np.arange(4)], - t_col='t') + df["t_new"] = np.ones(shape=(100,)) + dml_data = DoubleMLData(df, "y", "d", [f"Z{i + 1}" for i in np.arange(4)], t_col="t") # check that after changing t_col, the t array gets updated - t_comp = dml_data.data['t_new'].values - dml_data.t_col = 't_new' + t_comp = dml_data.data["t_new"].values + dml_data.t_col = "t_new" assert np.array_equal(dml_data.t, t_comp) - msg = r'Invalid time variable t_col. a13 is no data column.' + msg = r"Invalid time variable t_col. a13 is no data column." with pytest.raises(ValueError, match=msg): - dml_data.t_col = 'a13' + dml_data.t_col = "a13" - msg = (r'The time variable t_col must be of str type \(or None\). ' - "5 of type was passed.") + msg = r"The time variable t_col must be of str type \(or None\). " "5 of type was passed." with pytest.raises(TypeError, match=msg): dml_data.t_col = 5 @@ -444,22 +420,19 @@ def test_t_col_setter(): def test_s_col_setter(): np.random.seed(3141) df = make_ssm_data(n_obs=100, return_type=pd.DataFrame) - df['s_new'] = np.ones(shape=(100,)) - dml_data = DoubleMLData(df, 'y', 'd', - [f'X{i + 1}' for i in np.arange(4)], - s_col='s') + df["s_new"] = np.ones(shape=(100,)) + dml_data = DoubleMLData(df, "y", "d", [f"X{i + 1}" for i in np.arange(4)], s_col="s") # check that after changing s_col, the s array gets updated - s_comp = dml_data.data['s_new'].values - dml_data.s_col = 's_new' + s_comp = dml_data.data["s_new"].values + dml_data.s_col = "s_new" assert np.array_equal(dml_data.s, s_comp) - msg = r'Invalid score or selection variable s_col. a13 is no data column.' + msg = r"Invalid score or selection variable s_col. a13 is no data column." with pytest.raises(ValueError, match=msg): - dml_data.s_col = 'a13' + dml_data.s_col = "a13" - msg = (r'The score or selection variable s_col must be of str type \(or None\). ' - "5 of type was passed.") + msg = r"The score or selection variable s_col must be of str type \(or None\). " "5 of type was passed." with pytest.raises(TypeError, match=msg): dml_data.s_col = 5 @@ -473,34 +446,33 @@ def test_cluster_cols_setter(): np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f'X{i + 1}' for i in np.arange(7)] + ['y', 'd1', 'd2'] - dml_data = DoubleMLClusterData(df, 'y', ['d1', 'd2'], - cluster_cols=[f'X{i + 1}' for i in [5, 6]], - x_cols=[f'X{i + 1}' for i in np.arange(5)]) + df.columns = [f"X{i + 1}" for i in np.arange(7)] + ["y", "d1", "d2"] + dml_data = DoubleMLClusterData( + df, "y", ["d1", "d2"], cluster_cols=[f"X{i + 1}" for i in [5, 6]], x_cols=[f"X{i + 1}" for i in np.arange(5)] + ) - cluster_vars = df[['X6', 'X7']].values + cluster_vars = df[["X6", "X7"]].values assert np.array_equal(dml_data.cluster_vars, cluster_vars) assert dml_data.n_cluster_vars == 2 # check that after changing cluster_cols, the cluster_vars array gets updated - cluster_vars = df[['X7', 'X6']].values - dml_data.cluster_cols = ['X7', 'X6'] + cluster_vars = df[["X7", "X6"]].values + dml_data.cluster_cols = ["X7", "X6"] assert np.array_equal(dml_data.cluster_vars, cluster_vars) - msg = r'Invalid cluster variable\(s\) cluster_cols. At least one cluster variable is no data column.' + msg = r"Invalid cluster variable\(s\) cluster_cols. At least one cluster variable is no data column." with pytest.raises(ValueError, match=msg): - dml_data.cluster_cols = ['X6', 'X13'] + dml_data.cluster_cols = ["X6", "X13"] with pytest.raises(ValueError, match=msg): - dml_data.cluster_cols = 'X13' + dml_data.cluster_cols = "X13" - msg = (r'The cluster variable\(s\) cluster_cols must be of str or list type. ' - "5 of type was passed.") + msg = r"The cluster variable\(s\) cluster_cols must be of str or list type. " "5 of type was passed." with pytest.raises(TypeError, match=msg): dml_data.cluster_cols = 5 # check single cluster variable - cluster_vars = df[['X7']].values - dml_data.cluster_cols = 'X7' + cluster_vars = df[["X7"]].values + dml_data.cluster_cols = "X7" assert np.array_equal(dml_data.cluster_vars, cluster_vars) assert dml_data.n_cluster_vars == 1 @@ -510,20 +482,19 @@ def test_y_col_setter(): np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f'X{i + 1}' for i in np.arange(7)] + ['y', 'y123', 'd'] - dml_data = DoubleMLData(df, 'y', 'd', [f'X{i + 1}' for i in np.arange(7)]) + df.columns = [f"X{i + 1}" for i in np.arange(7)] + ["y", "y123", "d"] + dml_data = DoubleMLData(df, "y", "d", [f"X{i + 1}" for i in np.arange(7)]) # check that after changing y_col, the y array gets updated - y_comp = dml_data.data['y123'].values - dml_data.y_col = 'y123' + y_comp = dml_data.data["y123"].values + dml_data.y_col = "y123" assert np.array_equal(dml_data.y, y_comp) - msg = r'Invalid outcome variable y_col. d13 is no data column.' + msg = r"Invalid outcome variable y_col. d13 is no data column." with pytest.raises(ValueError, match=msg): - dml_data.y_col = 'd13' + dml_data.y_col = "d13" - msg = (r'The outcome variable y_col must be of str type. ' - "5 of type was passed.") + msg = r"The outcome variable y_col must be of str type. " "5 of type was passed." with pytest.raises(TypeError, match=msg): dml_data.y_col = 5 @@ -533,122 +504,123 @@ def test_use_other_treat_as_covariate(): np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f'X{i + 1}' for i in np.arange(7)] + ['y', 'd1', 'd2'] - dml_data = DoubleMLData(df, 'y', ['d1', 'd2'], [f'X{i + 1}' for i in np.arange(7)], - use_other_treat_as_covariate=True) - dml_data.set_x_d('d1') - assert np.array_equal(dml_data.d, df['d1'].values) - assert np.array_equal(dml_data.x, df[[f'X{i + 1}' for i in np.arange(7)] + ['d2']].values) - dml_data.set_x_d('d2') - assert np.array_equal(dml_data.d, df['d2'].values) - assert np.array_equal(dml_data.x, df[[f'X{i + 1}' for i in np.arange(7)] + ['d1']].values) - - dml_data = DoubleMLData(df, 'y', ['d1', 'd2'], [f'X{i + 1}' for i in np.arange(7)], - use_other_treat_as_covariate=False) - dml_data.set_x_d('d1') - assert np.array_equal(dml_data.d, df['d1'].values) - assert np.array_equal(dml_data.x, df[[f'X{i + 1}' for i in np.arange(7)]].values) - dml_data.set_x_d('d2') - assert np.array_equal(dml_data.d, df['d2'].values) - assert np.array_equal(dml_data.x, df[[f'X{i + 1}' for i in np.arange(7)]].values) + df.columns = [f"X{i + 1}" for i in np.arange(7)] + ["y", "d1", "d2"] + dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i + 1}" for i in np.arange(7)], use_other_treat_as_covariate=True) + dml_data.set_x_d("d1") + assert np.array_equal(dml_data.d, df["d1"].values) + assert np.array_equal(dml_data.x, df[[f"X{i + 1}" for i in np.arange(7)] + ["d2"]].values) + dml_data.set_x_d("d2") + assert np.array_equal(dml_data.d, df["d2"].values) + assert np.array_equal(dml_data.x, df[[f"X{i + 1}" for i in np.arange(7)] + ["d1"]].values) + + dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i + 1}" for i in np.arange(7)], use_other_treat_as_covariate=False) + dml_data.set_x_d("d1") + assert np.array_equal(dml_data.d, df["d1"].values) + assert np.array_equal(dml_data.x, df[[f"X{i + 1}" for i in np.arange(7)]].values) + dml_data.set_x_d("d2") + assert np.array_equal(dml_data.d, df["d2"].values) + assert np.array_equal(dml_data.x, df[[f"X{i + 1}" for i in np.arange(7)]].values) dml_data.use_other_treat_as_covariate = True - assert np.array_equal(dml_data.d, df['d1'].values) - assert np.array_equal(dml_data.x, df[[f'X{i + 1}' for i in np.arange(7)] + ['d2']].values) + assert np.array_equal(dml_data.d, df["d1"].values) + assert np.array_equal(dml_data.x, df[[f"X{i + 1}" for i in np.arange(7)] + ["d2"]].values) - msg = 'use_other_treat_as_covariate must be True or False. Got 1.' + msg = "use_other_treat_as_covariate must be True or False. Got 1." with pytest.raises(TypeError, match=msg): - _ = DoubleMLData(df, 'y', ['d1', 'd2'], [f'X{i + 1}' for i in np.arange(7)], - use_other_treat_as_covariate=1) + _ = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i + 1}" for i in np.arange(7)], use_other_treat_as_covariate=1) - msg = 'Invalid treatment_var. d3 is not in d_cols.' + msg = "Invalid treatment_var. d3 is not in d_cols." with pytest.raises(ValueError, match=msg): - dml_data.set_x_d('d3') + dml_data.set_x_d("d3") msg = r"treatment_var must be of str type. \['d1', 'd2'\] of type was passed." with pytest.raises(TypeError, match=msg): - dml_data.set_x_d(['d1', 'd2']) + dml_data.set_x_d(["d1", "d2"]) @pytest.mark.ci def test_disjoint_sets(): np.random.seed(3141) - df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), - columns=['yy', 'dd1', 'xx1', 'xx2', 'zz', 'tt']) + df = pd.DataFrame(np.tile(np.arange(6), (4, 1)), columns=["yy", "dd1", "xx1", "xx2", "zz", "tt"]) - msg = (r'At least one variable/column is set as treatment variable \(``d_cols``\) and as covariate\(``x_cols``\). ' - 'Consider using parameter ``use_other_treat_as_covariate``.') + msg = ( + r"At least one variable/column is set as treatment variable \(``d_cols``\) and as covariate\(``x_cols``\). " + "Consider using parameter ``use_other_treat_as_covariate``." + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1', 'xx1'], x_cols=['xx1', 'xx2']) - msg = 'yy cannot be set as outcome variable ``y_col`` and treatment variable in ``d_cols``' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1", "xx1"], x_cols=["xx1", "xx2"]) + msg = "yy cannot be set as outcome variable ``y_col`` and treatment variable in ``d_cols``" with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1', 'yy'], x_cols=['xx1', 'xx2']) - msg = 'yy cannot be set as outcome variable ``y_col`` and covariate in ``x_cols``' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1", "yy"], x_cols=["xx1", "xx2"]) + msg = "yy cannot be set as outcome variable ``y_col`` and covariate in ``x_cols``" with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'yy', 'xx2']) - msg = 'yy cannot be set as outcome variable ``y_col`` and instrumental variable in ``z_cols``' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "yy", "xx2"]) + msg = "yy cannot be set as outcome variable ``y_col`` and instrumental variable in ``z_cols``" with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], z_cols='yy') - msg = (r'At least one variable/column is set as treatment variable \(``d_cols``\) and instrumental variable in ' - '``z_cols``.') + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], z_cols="yy") + msg = ( + r"At least one variable/column is set as treatment variable \(``d_cols``\) and instrumental variable in " "``z_cols``." + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], z_cols=['dd1']) - msg = (r'At least one variable/column is set as covariate \(``x_cols``\) and instrumental variable in ' - '``z_cols``.') + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], z_cols=["dd1"]) + msg = r"At least one variable/column is set as covariate \(``x_cols``\) and instrumental variable in " "``z_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], z_cols='xx2') + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], z_cols="xx2") - msg = 'xx2 cannot be set as time variable ``t_col`` and covariate in ``x_cols``.' + msg = "xx2 cannot be set as time variable ``t_col`` and covariate in ``x_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], t_col='xx2') - msg = 'dd1 cannot be set as time variable ``t_col`` and treatment variable in ``d_cols``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], t_col="xx2") + msg = "dd1 cannot be set as time variable ``t_col`` and treatment variable in ``d_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], t_col='dd1') - msg = 'yy cannot be set as time variable ``t_col`` and outcome variable ``y_col``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], t_col="dd1") + msg = "yy cannot be set as time variable ``t_col`` and outcome variable ``y_col``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], t_col='yy') - msg = 'zz cannot be set as time variable ``t_col`` and instrumental variable in ``z_cols``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], t_col="yy") + msg = "zz cannot be set as time variable ``t_col`` and instrumental variable in ``z_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], z_cols='zz', t_col='zz') + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], z_cols="zz", t_col="zz") - msg = 'xx2 cannot be set as score or selection variable ``s_col`` and covariate in ``x_cols``.' + msg = "xx2 cannot be set as score or selection variable ``s_col`` and covariate in ``x_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], s_col='xx2') - msg = 'dd1 cannot be set as score or selection variable ``s_col`` and treatment variable in ``d_cols``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], s_col="xx2") + msg = "dd1 cannot be set as score or selection variable ``s_col`` and treatment variable in ``d_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], s_col='dd1') - msg = 'yy cannot be set as score or selection variable ``s_col`` and outcome variable ``y_col``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], s_col="dd1") + msg = "yy cannot be set as score or selection variable ``s_col`` and outcome variable ``y_col``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], s_col='yy') - msg = 'zz cannot be set as score or selection variable ``s_col`` and instrumental variable in ``z_cols``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], s_col="yy") + msg = "zz cannot be set as score or selection variable ``s_col`` and instrumental variable in ``z_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], z_cols='zz', s_col='zz') - msg = 'tt cannot be set as score or selection variable ``s_col`` and time variable ``t_col``.' + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], z_cols="zz", s_col="zz") + msg = "tt cannot be set as score or selection variable ``s_col`` and time variable ``t_col``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], t_col='tt', s_col='tt') + _ = DoubleMLData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], t_col="tt", s_col="tt") # cluster data - msg = 'yy cannot be set as outcome variable ``y_col`` and cluster variable in ``cluster_cols``' + msg = "yy cannot be set as outcome variable ``y_col`` and cluster variable in ``cluster_cols``" with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], cluster_cols='yy') - msg = (r'At least one variable/column is set as treatment variable \(``d_cols``\) and cluster variable in ' - '``cluster_cols``.') + _ = DoubleMLClusterData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], cluster_cols="yy") + msg = ( + r"At least one variable/column is set as treatment variable \(``d_cols``\) and cluster variable in " + "``cluster_cols``." + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], cluster_cols='dd1') - msg = (r'At least one variable/column is set as covariate \(``x_cols``\) and cluster variable in ' - '``cluster_cols``.') + _ = DoubleMLClusterData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], cluster_cols="dd1") + msg = r"At least one variable/column is set as covariate \(``x_cols``\) and cluster variable in " "``cluster_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1', 'xx2'], cluster_cols='xx2') - msg = (r'At least one variable/column is set as instrumental variable \(``z_cols``\) and cluster variable in ' - '``cluster_cols``.') + _ = DoubleMLClusterData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1", "xx2"], cluster_cols="xx2") + msg = ( + r"At least one variable/column is set as instrumental variable \(``z_cols``\) and cluster variable in " + "``cluster_cols``." + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1'], z_cols=['xx2'], cluster_cols='xx2') - msg = 'xx2 cannot be set as time variable ``t_col`` and cluster variable in ``cluster_cols``.' + _ = DoubleMLClusterData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1"], z_cols=["xx2"], cluster_cols="xx2") + msg = "xx2 cannot be set as time variable ``t_col`` and cluster variable in ``cluster_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1'], t_col='xx2', cluster_cols='xx2') - msg = 'xx2 cannot be set as score or selection variable ``s_col`` and cluster variable in ``cluster_cols``.' + _ = DoubleMLClusterData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1"], t_col="xx2", cluster_cols="xx2") + msg = "xx2 cannot be set as score or selection variable ``s_col`` and cluster variable in ``cluster_cols``." with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(df, y_col='yy', d_cols=['dd1'], x_cols=['xx1'], s_col='xx2', cluster_cols='xx2') + _ = DoubleMLClusterData(df, y_col="yy", d_cols=["dd1"], x_cols=["xx1"], s_col="xx2", cluster_cols="xx2") @pytest.mark.ci @@ -657,38 +629,39 @@ def test_duplicates(): dml_data = make_plr_CCDDHNR2018(n_obs=100) dml_cluster_data = make_pliv_multiway_cluster_CKMS2021(N=10, M=10) - msg = r'Invalid treatment variable\(s\) d_cols: Contains duplicate values.' + msg = r"Invalid treatment variable\(s\) d_cols: Contains duplicate values." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(dml_data.data, y_col='y', d_cols=['d', 'd', 'X1'], x_cols=['X3', 'X2']) + _ = DoubleMLData(dml_data.data, y_col="y", d_cols=["d", "d", "X1"], x_cols=["X3", "X2"]) with pytest.raises(ValueError, match=msg): - dml_data.d_cols = ['d', 'd', 'X1'] + dml_data.d_cols = ["d", "d", "X1"] - msg = 'Invalid covariates x_cols: Contains duplicate values.' + msg = "Invalid covariates x_cols: Contains duplicate values." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(dml_data.data, y_col='y', d_cols=['d'], x_cols=['X3', 'X2', 'X3']) + _ = DoubleMLData(dml_data.data, y_col="y", d_cols=["d"], x_cols=["X3", "X2", "X3"]) with pytest.raises(ValueError, match=msg): - dml_data.x_cols = ['X3', 'X2', 'X3'] + dml_data.x_cols = ["X3", "X2", "X3"] - msg = r'Invalid instrumental variable\(s\) z_cols: Contains duplicate values.' + msg = r"Invalid instrumental variable\(s\) z_cols: Contains duplicate values." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(dml_data.data, y_col='y', d_cols=['d'], x_cols=['X3', 'X2'], - z_cols=['X15', 'X12', 'X12', 'X15']) + _ = DoubleMLData(dml_data.data, y_col="y", d_cols=["d"], x_cols=["X3", "X2"], z_cols=["X15", "X12", "X12", "X15"]) with pytest.raises(ValueError, match=msg): - dml_data.z_cols = ['X15', 'X12', 'X12', 'X15'] + dml_data.z_cols = ["X15", "X12", "X12", "X15"] - msg = r'Invalid cluster variable\(s\) cluster_cols: Contains duplicate values.' + msg = r"Invalid cluster variable\(s\) cluster_cols: Contains duplicate values." with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(dml_cluster_data.data, y_col='y', d_cols=['d'], cluster_cols=['X3', 'X2', 'X3']) + _ = DoubleMLClusterData(dml_cluster_data.data, y_col="y", d_cols=["d"], cluster_cols=["X3", "X2", "X3"]) with pytest.raises(ValueError, match=msg): - dml_cluster_data.cluster_cols = ['X3', 'X2', 'X3'] + dml_cluster_data.cluster_cols = ["X3", "X2", "X3"] - msg = 'Invalid pd.DataFrame: Contains duplicate column names.' + msg = "Invalid pd.DataFrame: Contains duplicate column names." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(pd.DataFrame(np.zeros((100, 5)), columns=['y', 'd', 'X3', 'X2', 'y']), - y_col='y', d_cols=['d'], x_cols=['X3', 'X2']) + _ = DoubleMLData( + pd.DataFrame(np.zeros((100, 5)), columns=["y", "d", "X3", "X2", "y"]), y_col="y", d_cols=["d"], x_cols=["X3", "X2"] + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLClusterData(pd.DataFrame(np.zeros((100, 5)), columns=['y', 'd', 'X3', 'X2', 'y']), - y_col='y', d_cols=['d'], cluster_cols=['X2']) + _ = DoubleMLClusterData( + pd.DataFrame(np.zeros((100, 5)), columns=["y", "d", "X3", "X2", "y"]), y_col="y", d_cols=["d"], cluster_cols=["X2"] + ) @pytest.mark.ci @@ -697,62 +670,49 @@ def test_dml_datatype(): # msg = ('data must be of pd.DataFrame type. ' # f'{str(data_array)} of type {str(type(data_array))} was passed.') with pytest.raises(TypeError): - _ = DoubleMLData(data_array, y_col='y', d_cols=['d'], x_cols=['X3', 'X2']) + _ = DoubleMLData(data_array, y_col="y", d_cols=["d"], x_cols=["X3", "X2"]) with pytest.raises(TypeError): - _ = DoubleMLClusterData(data_array, y_col='y', d_cols=['d'], cluster_cols=['X3', 'X2']) + _ = DoubleMLClusterData(data_array, y_col="y", d_cols=["d"], cluster_cols=["X3", "X2"]) @pytest.mark.ci def test_dml_data_w_missings(generate_data_irm_w_missings): (x, y, d) = generate_data_irm_w_missings - dml_data = DoubleMLData.from_arrays(x, y, d, - force_all_x_finite=False) + dml_data = DoubleMLData.from_arrays(x, y, d, force_all_x_finite=False) - _ = DoubleMLData.from_arrays(x, y, d, - force_all_x_finite='allow-nan') + _ = DoubleMLData.from_arrays(x, y, d, force_all_x_finite="allow-nan") msg = r"Input contains NaN." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData.from_arrays(x, y, d, - force_all_x_finite=True) + _ = DoubleMLData.from_arrays(x, y, d, force_all_x_finite=True) with pytest.raises(ValueError, match=msg): - _ = DoubleMLData.from_arrays(x, x[:, 0], d, - force_all_x_finite=False) + _ = DoubleMLData.from_arrays(x, x[:, 0], d, force_all_x_finite=False) with pytest.raises(ValueError, match=msg): - _ = DoubleMLData.from_arrays(x, y, x[:, 0], - force_all_x_finite=False) + _ = DoubleMLData.from_arrays(x, y, x[:, 0], force_all_x_finite=False) with pytest.raises(ValueError, match=msg): - _ = DoubleMLData.from_arrays(x, y, d, x[:, 0], - force_all_x_finite=False) + _ = DoubleMLData.from_arrays(x, y, d, x[:, 0], force_all_x_finite=False) msg = r"Input contains infinity or a value too large for dtype\('float64'\)." xx = np.copy(x) xx[0, 0] = np.inf with pytest.raises(ValueError, match=msg): - _ = DoubleMLData.from_arrays(xx, y, d, - force_all_x_finite='allow-nan') + _ = DoubleMLData.from_arrays(xx, y, d, force_all_x_finite="allow-nan") msg = "Invalid force_all_x_finite. force_all_x_finite must be True, False or 'allow-nan'." with pytest.raises(TypeError, match=msg): - _ = DoubleMLData.from_arrays(xx, y, d, - force_all_x_finite=1) + _ = DoubleMLData.from_arrays(xx, y, d, force_all_x_finite=1) with pytest.raises(TypeError, match=msg): - _ = DoubleMLData(dml_data.data, - y_col='y', d_cols='d', - force_all_x_finite=1) + _ = DoubleMLData(dml_data.data, y_col="y", d_cols="d", force_all_x_finite=1) msg = "Invalid force_all_x_finite allownan. force_all_x_finite must be True, False or 'allow-nan'." with pytest.raises(ValueError, match=msg): - _ = DoubleMLData.from_arrays(xx, y, d, - force_all_x_finite='allownan') + _ = DoubleMLData.from_arrays(xx, y, d, force_all_x_finite="allownan") with pytest.raises(ValueError, match=msg): - _ = DoubleMLData(dml_data.data, - y_col='y', d_cols='d', - force_all_x_finite='allownan') + _ = DoubleMLData(dml_data.data, y_col="y", d_cols="d", force_all_x_finite="allownan") msg = r"Input contains NaN." with pytest.raises(ValueError, match=msg): @@ -761,5 +721,5 @@ def test_dml_data_w_missings(generate_data_irm_w_missings): assert dml_data.force_all_x_finite is True dml_data.force_all_x_finite = False assert dml_data.force_all_x_finite is False - dml_data.force_all_x_finite = 'allow-nan' - assert dml_data.force_all_x_finite == 'allow-nan' + dml_data.force_all_x_finite = "allow-nan" + assert dml_data.force_all_x_finite == "allow-nan" diff --git a/doubleml/tests/test_evaluate_learner.py b/doubleml/tests/test_evaluate_learner.py index 1e5b96496..dbad9b620 100644 --- a/doubleml/tests/test_evaluate_learner.py +++ b/doubleml/tests/test_evaluate_learner.py @@ -9,65 +9,68 @@ from doubleml.utils._estimation import _logloss np.random.seed(3141) -data = make_irm_data(theta=0.5, n_obs=200, dim_x=5, return_type='DataFrame') -obj_dml_data = dml.DoubleMLData(data, 'y', 'd') +data = make_irm_data(theta=0.5, n_obs=200, dim_x=5, return_type="DataFrame") +obj_dml_data = dml.DoubleMLData(data, "y", "d") -@pytest.fixture(scope='module', - params=[[LinearRegression(), - LogisticRegression(solver='lbfgs', max_iter=250)], - [RandomForestRegressor(max_depth=2, n_estimators=10), - RandomForestClassifier(max_depth=2, n_estimators=10)]]) +@pytest.fixture( + scope="module", + params=[ + [LinearRegression(), LogisticRegression(solver="lbfgs", max_iter=250)], + [RandomForestRegressor(max_depth=2, n_estimators=10), RandomForestClassifier(max_depth=2, n_estimators=10)], + ], +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=[1, 5]) +@pytest.fixture(scope="module", params=[1, 5]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[0.01, 0.05]) +@pytest.fixture(scope="module", params=[0.01, 0.05]) def trimming_threshold(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_irm_eval_learner_fixture(learner, trimming_threshold, n_rep): # Set machine learning methods for m & g ml_g = clone(learner[0]) ml_m = clone(learner[1]) np.random.seed(3141) - dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, - ml_g, ml_m, - n_folds=2, - n_rep=n_rep, - trimming_threshold=trimming_threshold) + dml_irm_obj = dml.DoubleMLIRM(obj_dml_data, ml_g, ml_m, n_folds=2, n_rep=n_rep, trimming_threshold=trimming_threshold) dml_irm_obj.fit() - res_manual = dml_irm_obj.evaluate_learners(learners=['ml_g0', 'ml_g1']) - res_manual['ml_m'] = dml_irm_obj.evaluate_learners(learners=['ml_m'], metric=_logloss)['ml_m'] + res_manual = dml_irm_obj.evaluate_learners(learners=["ml_g0", "ml_g1"]) + res_manual["ml_m"] = dml_irm_obj.evaluate_learners(learners=["ml_m"], metric=_logloss)["ml_m"] - res_dict = {'nuisance_loss': dml_irm_obj.nuisance_loss, - 'nuisance_loss_manual': res_manual - } + res_dict = {"nuisance_loss": dml_irm_obj.nuisance_loss, "nuisance_loss_manual": res_manual} return res_dict @pytest.mark.ci def test_dml_irm_eval_learner(dml_irm_eval_learner_fixture, n_rep): - assert dml_irm_eval_learner_fixture['nuisance_loss_manual']['ml_g0'].shape == (n_rep, 1) - assert dml_irm_eval_learner_fixture['nuisance_loss_manual']['ml_g1'].shape == (n_rep, 1) - assert dml_irm_eval_learner_fixture['nuisance_loss_manual']['ml_m'].shape == (n_rep, 1) + assert dml_irm_eval_learner_fixture["nuisance_loss_manual"]["ml_g0"].shape == (n_rep, 1) + assert dml_irm_eval_learner_fixture["nuisance_loss_manual"]["ml_g1"].shape == (n_rep, 1) + assert dml_irm_eval_learner_fixture["nuisance_loss_manual"]["ml_m"].shape == (n_rep, 1) - assert np.allclose(dml_irm_eval_learner_fixture['nuisance_loss_manual']['ml_g0'], - dml_irm_eval_learner_fixture['nuisance_loss']['ml_g0'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_irm_eval_learner_fixture['nuisance_loss_manual']['ml_g1'], - dml_irm_eval_learner_fixture['nuisance_loss']['ml_g1'], - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_irm_eval_learner_fixture['nuisance_loss_manual']['ml_m'], - dml_irm_eval_learner_fixture['nuisance_loss']['ml_m'], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_irm_eval_learner_fixture["nuisance_loss_manual"]["ml_g0"], + dml_irm_eval_learner_fixture["nuisance_loss"]["ml_g0"], + rtol=1e-9, + atol=1e-4, + ) + assert np.allclose( + dml_irm_eval_learner_fixture["nuisance_loss_manual"]["ml_g1"], + dml_irm_eval_learner_fixture["nuisance_loss"]["ml_g1"], + rtol=1e-9, + atol=1e-4, + ) + assert np.allclose( + dml_irm_eval_learner_fixture["nuisance_loss_manual"]["ml_m"], + dml_irm_eval_learner_fixture["nuisance_loss"]["ml_m"], + rtol=1e-9, + atol=1e-4, + ) diff --git a/doubleml/tests/test_exceptions.py b/doubleml/tests/test_exceptions.py index 4ddd1384d..d8cc05a0f 100644 --- a/doubleml/tests/test_exceptions.py +++ b/doubleml/tests/test_exceptions.py @@ -40,7 +40,7 @@ ml_g = Lasso() ml_r = Lasso() dml_plr = DoubleMLPLR(dml_data, ml_l, ml_m) -dml_plr_iv_type = DoubleMLPLR(dml_data, ml_l, ml_m, ml_g, score='IV-type') +dml_plr_iv_type = DoubleMLPLR(dml_data, ml_l, ml_m, ml_g, score="IV-type") dml_data_pliv = make_pliv_CHS2015(n_obs=n, dim_z=1) dml_pliv = DoubleMLPLIV(dml_data_pliv, ml_l, ml_m, ml_r) @@ -59,11 +59,11 @@ @pytest.mark.ci def test_doubleml_exception_data(): - msg = 'The data must be of DoubleMLData or DoubleMLClusterData type.' + msg = "The data must be of DoubleMLData or DoubleMLClusterData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(pd.DataFrame(), ml_l, ml_m) - msg = 'The data must be of DoubleMLData type.' + msg = "The data must be of DoubleMLData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_l, ml_m) with pytest.raises(TypeError, match=msg): @@ -80,207 +80,217 @@ def test_doubleml_exception_data(): _ = DoubleMLCVAR(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_g, ml_m, treatment=1) with pytest.raises(TypeError, match=msg): _ = DoubleMLQTE(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_g, ml_m) - msg = 'For repeated outcomes the data must be of DoubleMLData type.' + msg = "For repeated outcomes the data must be of DoubleMLData type." with pytest.raises(TypeError, match=msg): _ = DoubleMLDID(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_g, ml_m) - msg = 'For repeated cross sections the data must be of DoubleMLData type. ' + msg = "For repeated cross sections the data must be of DoubleMLData type. " with pytest.raises(TypeError, match=msg): _ = DoubleMLDIDCS(DummyDataClass(pd.DataFrame(np.zeros((100, 10)))), ml_g, ml_m) # PLR with IV - msg = (r'Incompatible data. Z1 have been set as instrumental variable\(s\). ' - 'To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR.') + msg = ( + r"Incompatible data. Z1 have been set as instrumental variable\(s\). " + "To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data_pliv, ml_l, ml_m) # PLIV without IV - msg = ('Incompatible data. ' - 'At least one variable must be set as instrumental variable. ' - r'To fit a partially linear regression model without instrumental variable\(s\) ' - 'use DoubleMLPLR instead of DoubleMLPLIV.') + msg = ( + "Incompatible data. " + "At least one variable must be set as instrumental variable. " + r"To fit a partially linear regression model without instrumental variable\(s\) " + "use DoubleMLPLR instead of DoubleMLPLIV." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLPLIV(dml_data, Lasso(), Lasso(), Lasso()) # IRM with IV - msg = (r'Incompatible data. z have been set as instrumental variable\(s\). ' - 'To fit an interactive IV regression model use DoubleMLIIVM instead of DoubleMLIRM.') + msg = ( + r"Incompatible data. z have been set as instrumental variable\(s\). " + "To fit an interactive IV regression model use DoubleMLIIVM instead of DoubleMLIRM." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLIRM(dml_data_iivm, Lasso(), LogisticRegression()) - msg = ('Incompatible data. To fit an IRM model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an IRM model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_irm = dml_data_irm.data.copy() - df_irm['d'] = df_irm['d'] * 2 + df_irm["d"] = df_irm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for IRM - _ = DoubleMLIRM(DoubleMLData(df_irm, 'y', 'd'), - Lasso(), LogisticRegression()) + _ = DoubleMLIRM(DoubleMLData(df_irm, "y", "d"), Lasso(), LogisticRegression()) with pytest.raises(ValueError, match=msg): # multiple D for IRM - _ = DoubleMLIRM(DoubleMLData(df_irm, 'y', ['d', 'X1']), - Lasso(), LogisticRegression()) + _ = DoubleMLIRM(DoubleMLData(df_irm, "y", ["d", "X1"]), Lasso(), LogisticRegression()) - msg = ('Incompatible data. To fit an IIVM model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an IIVM model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_iivm = dml_data_iivm.data.copy() - df_iivm['d'] = df_iivm['d'] * 2 + df_iivm["d"] = df_iivm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for IIVM - _ = DoubleMLIIVM(DoubleMLData(df_iivm, 'y', 'd', z_cols='z'), - Lasso(), LogisticRegression(), LogisticRegression()) + _ = DoubleMLIIVM(DoubleMLData(df_iivm, "y", "d", z_cols="z"), Lasso(), LogisticRegression(), LogisticRegression()) df_iivm = dml_data_iivm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for IIVM - _ = DoubleMLIIVM(DoubleMLData(df_iivm, 'y', ['d', 'X1'], z_cols='z'), - Lasso(), LogisticRegression(), LogisticRegression()) + _ = DoubleMLIIVM( + DoubleMLData(df_iivm, "y", ["d", "X1"], z_cols="z"), Lasso(), LogisticRegression(), LogisticRegression() + ) - msg = ('Incompatible data. To fit an IIVM model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as instrumental variable.') + msg = ( + "Incompatible data. To fit an IIVM model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as instrumental variable." + ) with pytest.raises(ValueError, match=msg): # IIVM without IV - _ = DoubleMLIIVM(dml_data_irm, - Lasso(), LogisticRegression(), LogisticRegression()) + _ = DoubleMLIIVM(dml_data_irm, Lasso(), LogisticRegression(), LogisticRegression()) df_iivm = dml_data_iivm.data.copy() - df_iivm['z'] = df_iivm['z'] * 2 + df_iivm["z"] = df_iivm["z"] * 2 with pytest.raises(ValueError, match=msg): # non-binary Z for IIVM - _ = DoubleMLIIVM(DoubleMLData(df_iivm, 'y', 'd', z_cols='z'), - Lasso(), LogisticRegression(), LogisticRegression()) + _ = DoubleMLIIVM(DoubleMLData(df_iivm, "y", "d", z_cols="z"), Lasso(), LogisticRegression(), LogisticRegression()) df_iivm = dml_data_iivm.data.copy() with pytest.raises(ValueError, match=msg): # multiple Z for IIVM - _ = DoubleMLIIVM(DoubleMLData(df_iivm, 'y', 'd', z_cols=['z', 'X1']), - Lasso(), LogisticRegression(), LogisticRegression()) + _ = DoubleMLIIVM( + DoubleMLData(df_iivm, "y", "d", z_cols=["z", "X1"]), Lasso(), LogisticRegression(), LogisticRegression() + ) # PQ with IV - msg = r'Incompatible data. z have been set as instrumental variable\(s\).' + msg = r"Incompatible data. z have been set as instrumental variable\(s\)." with pytest.raises(ValueError, match=msg): _ = DoubleMLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1) - msg = ('Incompatible data. To fit an PQ model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an PQ model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_irm = dml_data_irm.data.copy() - df_irm['d'] = df_irm['d'] * 2 + df_irm["d"] = df_irm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for PQ - _ = DoubleMLPQ(DoubleMLData(df_irm, 'y', 'd'), - LogisticRegression(), LogisticRegression(), treatment=1) + _ = DoubleMLPQ(DoubleMLData(df_irm, "y", "d"), LogisticRegression(), LogisticRegression(), treatment=1) df_irm = dml_data_irm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for PQ - _ = DoubleMLPQ(DoubleMLData(df_irm, 'y', ['d', 'X1']), - LogisticRegression(), LogisticRegression(), treatment=1) + _ = DoubleMLPQ(DoubleMLData(df_irm, "y", ["d", "X1"]), LogisticRegression(), LogisticRegression(), treatment=1) # LPQ with non-binary treatment - msg = ('Incompatible data. To fit an LPQ model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an LPQ model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_iivm = dml_data_iivm.data.copy() - df_iivm['d'] = df_iivm['d'] * 2 + df_iivm["d"] = df_iivm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for LPQ - _ = DoubleMLLPQ(DoubleMLData(df_iivm, 'y', 'd', 'z'), - LogisticRegression(), LogisticRegression(), treatment=1) + _ = DoubleMLLPQ(DoubleMLData(df_iivm, "y", "d", "z"), LogisticRegression(), LogisticRegression(), treatment=1) df_iivm = dml_data_iivm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for LPQ - _ = DoubleMLLPQ(DoubleMLData(df_iivm, 'y', ['d', 'X1'], 'z'), - LogisticRegression(), LogisticRegression(), treatment=1) - msg = ('Incompatible data. To fit an LPQ model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as instrumental variable.') + _ = DoubleMLLPQ(DoubleMLData(df_iivm, "y", ["d", "X1"], "z"), LogisticRegression(), LogisticRegression(), treatment=1) + msg = ( + "Incompatible data. To fit an LPQ model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as instrumental variable." + ) df_iivm = dml_data_iivm.data.copy() - df_iivm['z'] = df_iivm['z'] * 2 + df_iivm["z"] = df_iivm["z"] * 2 with pytest.raises(ValueError, match=msg): # no instrument Z for LPQ - _ = DoubleMLLPQ(DoubleMLData(df_iivm, 'y', 'd', x_cols=['z']), - LogisticRegression(), LogisticRegression(), treatment=1) + _ = DoubleMLLPQ(DoubleMLData(df_iivm, "y", "d", x_cols=["z"]), LogisticRegression(), LogisticRegression(), treatment=1) with pytest.raises(ValueError, match=msg): # non-binary Z for LPQ - _ = DoubleMLLPQ(DoubleMLData(df_iivm, 'y', 'd', z_cols=['z']), - LogisticRegression(), LogisticRegression(), treatment=1) + _ = DoubleMLLPQ(DoubleMLData(df_iivm, "y", "d", z_cols=["z"]), LogisticRegression(), LogisticRegression(), treatment=1) # CVAR with IV - msg = r'Incompatible data. z have been set as instrumental variable\(s\).' + msg = r"Incompatible data. z have been set as instrumental variable\(s\)." with pytest.raises(ValueError, match=msg): _ = DoubleMLCVAR(dml_data_iivm, Lasso(), LogisticRegression(), treatment=1) - msg = ('Incompatible data. To fit an CVaR model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an CVaR model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_irm = dml_data_irm.data.copy() - df_irm['d'] = df_irm['d'] * 2 + df_irm["d"] = df_irm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for CVAR - _ = DoubleMLCVAR(DoubleMLData(df_irm, 'y', 'd'), - Lasso(), LogisticRegression(), treatment=1) + _ = DoubleMLCVAR(DoubleMLData(df_irm, "y", "d"), Lasso(), LogisticRegression(), treatment=1) df_irm = dml_data_irm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for CVAR - _ = DoubleMLCVAR(DoubleMLData(df_irm, 'y', ['d', 'X1']), - Lasso(), LogisticRegression(), treatment=1) + _ = DoubleMLCVAR(DoubleMLData(df_irm, "y", ["d", "X1"]), Lasso(), LogisticRegression(), treatment=1) # QTE - msg = ('Incompatible data. To fit an PQ model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an PQ model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_irm = dml_data_irm.data.copy() - df_irm['d'] = df_irm['d'] * 2 + df_irm["d"] = df_irm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for QTE - _ = DoubleMLQTE(DoubleMLData(df_irm, 'y', 'd'), - LogisticRegression(), LogisticRegression()) + _ = DoubleMLQTE(DoubleMLData(df_irm, "y", "d"), LogisticRegression(), LogisticRegression()) df_irm = dml_data_irm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for QTE - _ = DoubleMLQTE(DoubleMLData(df_irm, 'y', ['d', 'X1']), - LogisticRegression(), LogisticRegression()) + _ = DoubleMLQTE(DoubleMLData(df_irm, "y", ["d", "X1"]), LogisticRegression(), LogisticRegression()) # DID with IV - msg = r'Incompatible data. z have been set as instrumental variable\(s\).' + msg = r"Incompatible data. z have been set as instrumental variable\(s\)." with pytest.raises(ValueError, match=msg): _ = DoubleMLDID(dml_data_iivm, Lasso(), LogisticRegression()) - msg = ('Incompatible data. To fit an DID model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an DID model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_irm = dml_data_irm.data.copy() - df_irm['d'] = df_irm['d'] * 2 + df_irm["d"] = df_irm["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for DID - _ = DoubleMLDID(DoubleMLData(df_irm, 'y', 'd'), - Lasso(), LogisticRegression()) + _ = DoubleMLDID(DoubleMLData(df_irm, "y", "d"), Lasso(), LogisticRegression()) df_irm = dml_data_irm.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for DID - _ = DoubleMLDID(DoubleMLData(df_irm, 'y', ['d', 'X1']), - Lasso(), LogisticRegression()) + _ = DoubleMLDID(DoubleMLData(df_irm, "y", ["d", "X1"]), Lasso(), LogisticRegression()) # DIDCS with IV - msg = r'Incompatible data. z have been set as instrumental variable\(s\).' + msg = r"Incompatible data. z have been set as instrumental variable\(s\)." with pytest.raises(ValueError, match=msg): _ = DoubleMLDIDCS(dml_data_iivm, Lasso(), LogisticRegression()) # DIDCS treatment exceptions - msg = ('Incompatible data. To fit an DIDCS model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + msg = ( + "Incompatible data. To fit an DIDCS model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) df_did_cs = dml_data_did_cs.data.copy() - df_did_cs['d'] = df_did_cs['d'] * 2 + df_did_cs["d"] = df_did_cs["d"] * 2 with pytest.raises(ValueError, match=msg): # non-binary D for DIDCS - _ = DoubleMLDIDCS(DoubleMLData(df_did_cs, y_col='y', d_cols='d', t_col='t'), - Lasso(), LogisticRegression()) + _ = DoubleMLDIDCS(DoubleMLData(df_did_cs, y_col="y", d_cols="d", t_col="t"), Lasso(), LogisticRegression()) df_did_cs = dml_data_did_cs.data.copy() with pytest.raises(ValueError, match=msg): # multiple D for DIDCS - _ = DoubleMLDIDCS(DoubleMLData(df_did_cs, y_col='y', d_cols=['d', 'Z1'], t_col='t'), - Lasso(), LogisticRegression()) + _ = DoubleMLDIDCS(DoubleMLData(df_did_cs, y_col="y", d_cols=["d", "Z1"], t_col="t"), Lasso(), LogisticRegression()) # DIDCS time exceptions - msg = ('Incompatible data. To fit an DIDCS model with DML exactly one binary variable with values 0 and 1 ' - 'needs to be specified as time variable.') + msg = ( + "Incompatible data. To fit an DIDCS model with DML exactly one binary variable with values 0 and 1 " + "needs to be specified as time variable." + ) df_did_cs = dml_data_did_cs.data.copy() - df_did_cs['t'] = df_did_cs['t'] * 2 + df_did_cs["t"] = df_did_cs["t"] * 2 with pytest.raises(ValueError, match=msg): # non-binary t for DIDCS - _ = DoubleMLDIDCS(DoubleMLData(df_did_cs, y_col='y', d_cols='d', t_col='t'), - Lasso(), LogisticRegression()) + _ = DoubleMLDIDCS(DoubleMLData(df_did_cs, y_col="y", d_cols="d", t_col="t"), Lasso(), LogisticRegression()) @pytest.mark.ci def test_doubleml_exception_framework(): - msg = r'Apply fit\(\) before sensitivity_analysis\(\).' + msg = r"Apply fit\(\) before sensitivity_analysis\(\)." with pytest.raises(ValueError, match=msg): dml_obj = DoubleMLPLR(dml_data, ml_l, ml_m) dml_obj.sensitivity_analysis() @@ -289,158 +299,192 @@ def test_doubleml_exception_framework(): @pytest.mark.ci def test_doubleml_exception_scores(): # PLR - msg = 'Invalid score IV. Valid score IV-type or partialling out.' + msg = "Invalid score IV. Valid score IV-type or partialling out." with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_l, ml_m, score='IV') - msg = 'score should be either a string or a callable. 0 was passed.' + _ = DoubleMLPLR(dml_data, ml_l, ml_m, score="IV") + msg = "score should be either a string or a callable. 0 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, score=0) # IRM - msg = 'Invalid score IV. Valid score ATE or ATTE.' + msg = "Invalid score IV. Valid score ATE or ATTE." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), score='IV') - msg = 'score should be either a string or a callable. 0 was passed.' + _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), score="IV") + msg = "score should be either a string or a callable. 0 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), score=0) # IIVM - msg = 'Invalid score ATE. Valid score LATE.' + msg = "Invalid score ATE. Valid score LATE." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), score='ATE') - msg = 'score should be either a string or a callable. 0 was passed.' + _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), score="ATE") + msg = "score should be either a string or a callable. 0 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), score=0) # PLIV - msg = 'Invalid score IV. Valid score partialling out.' + msg = "Invalid score IV. Valid score partialling out." with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso(), score='IV') - msg = 'score should be either a string or a callable. 0 was passed.' + _ = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso(), score="IV") + msg = "score should be either a string or a callable. 0 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso(), score=0) # PQ - msg = 'Invalid score IV. Valid score PQ.' + msg = "Invalid score IV. Valid score PQ." with pytest.raises(ValueError, match=msg): - _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, score=2) # LPQ - msg = 'Invalid score IV. Valid score LPQ.' + msg = "Invalid score IV. Valid score LPQ." with pytest.raises(ValueError, match=msg): - _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, score=2) # CVaR - msg = 'Invalid score IV. Valid score CVaR.' + msg = "Invalid score IV. Valid score CVaR." with pytest.raises(ValueError, match=msg): - _ = DoubleMLCVAR(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLCVAR(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLCVAR(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, score=2) # QTE - msg = 'Invalid score IV. Valid score PQ or LPQ or CVaR.' + msg = "Invalid score IV. Valid score PQ or LPQ or CVaR." with pytest.raises(ValueError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), score=2) # DID - msg = 'Invalid score IV. Valid score observational or experimental.' + msg = "Invalid score IV. Valid score observational or experimental." with pytest.raises(ValueError, match=msg): - _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), score=2) # DIDCS - msg = 'Invalid score IV. Valid score observational or experimental.' + msg = "Invalid score IV. Valid score observational or experimental." with pytest.raises(ValueError, match=msg): - _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), score='IV') - msg = 'score should be a string. 2 was passed.' + _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), score="IV") + msg = "score should be a string. 2 was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), score=2) @pytest.mark.ci def test_doubleml_exception_trimming_rule(): - msg = 'Invalid trimming_rule discard. Valid trimming_rule truncate.' + msg = "Invalid trimming_rule discard. Valid trimming_rule truncate." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), trimming_rule='discard') + _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), trimming_rule='discard') + _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, trimming_rule='discard') + _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, trimming_rule='discard') + _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLCVAR(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, trimming_rule='discard') + _ = DoubleMLCVAR(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule='discard') + _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), trimming_rule='discard') + _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), trimming_rule="discard") with pytest.raises(ValueError, match=msg): - _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), trimming_rule='discard') + _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), trimming_rule="discard") # check the trimming_threshold exceptions msg = "trimming_threshold has to be a float. Object of type passed." with pytest.raises(TypeError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLCVAR(dml_data_irm, Lasso(), LogisticRegression(), treatment=1, - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold="0.1") - with pytest.raises(TypeError, match=msg): - _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold="0.1") - - msg = 'Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5.' - with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLPQ(dml_data_irm, LogisticRegression(), LogisticRegression(), treatment=1, - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLLPQ(dml_data_iivm, LogisticRegression(), LogisticRegression(), treatment=1, - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLCVAR(dml_data_irm, Lasso(), LogisticRegression(), treatment=1, - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLQTE(dml_data_irm, LogisticRegression(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold=0.6) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), - trimming_rule='truncate', trimming_threshold=0.6) + _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), trimming_rule="truncate", trimming_threshold="0.1") + with pytest.raises(TypeError, match=msg): + _ = DoubleMLIIVM( + dml_data_iivm, + Lasso(), + LogisticRegression(), + LogisticRegression(), + trimming_rule="truncate", + trimming_threshold="0.1", + ) + with pytest.raises(TypeError, match=msg): + _ = DoubleMLPQ( + dml_data_irm, + LogisticRegression(), + LogisticRegression(), + treatment=1, + trimming_rule="truncate", + trimming_threshold="0.1", + ) + with pytest.raises(TypeError, match=msg): + _ = DoubleMLLPQ( + dml_data_iivm, + LogisticRegression(), + LogisticRegression(), + treatment=1, + trimming_rule="truncate", + trimming_threshold="0.1", + ) + with pytest.raises(TypeError, match=msg): + _ = DoubleMLCVAR( + dml_data_irm, Lasso(), LogisticRegression(), treatment=1, trimming_rule="truncate", trimming_threshold="0.1" + ) + with pytest.raises(TypeError, match=msg): + _ = DoubleMLQTE( + dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule="truncate", trimming_threshold="0.1" + ) + with pytest.raises(TypeError, match=msg): + _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), trimming_rule="truncate", trimming_threshold="0.1") + with pytest.raises(TypeError, match=msg): + _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), trimming_rule="truncate", trimming_threshold="0.1") + + msg = "Invalid trimming_threshold 0.6. trimming_threshold has to be between 0 and 0.5." + with pytest.raises(ValueError, match=msg): + _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), trimming_rule="truncate", trimming_threshold=0.6) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLIIVM( + dml_data_iivm, + Lasso(), + LogisticRegression(), + LogisticRegression(), + trimming_rule="truncate", + trimming_threshold=0.6, + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLPQ( + dml_data_irm, + LogisticRegression(), + LogisticRegression(), + treatment=1, + trimming_rule="truncate", + trimming_threshold=0.6, + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLLPQ( + dml_data_iivm, + LogisticRegression(), + LogisticRegression(), + treatment=1, + trimming_rule="truncate", + trimming_threshold=0.6, + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLCVAR( + dml_data_irm, Lasso(), LogisticRegression(), treatment=1, trimming_rule="truncate", trimming_threshold=0.6 + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLQTE( + dml_data_irm, LogisticRegression(), LogisticRegression(), trimming_rule="truncate", trimming_threshold=0.6 + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), trimming_rule="truncate", trimming_threshold=0.6) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), trimming_rule="truncate", trimming_threshold=0.6) @pytest.mark.ci @@ -451,11 +495,12 @@ def test_doubleml_exception_weights(): _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), weights=1) msg = r"weights must have keys \['weights', 'weights_bar'\]. keys dict_keys\(\['d'\]\) were passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), weights={'d': [1, 2, 3]}) + _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), weights={"d": [1, 2, 3]}) msg = "weights must be a numpy array for ATTE score. weights of type was passed." with pytest.raises(TypeError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - score='ATTE', weights={'weights': np.ones_like(dml_data_irm.d)}) + _ = DoubleMLIRM( + dml_data_irm, Lasso(), LogisticRegression(), score="ATTE", weights={"weights": np.ones_like(dml_data_irm.d)} + ) # shape checks msg = rf"weights must have shape \({n},\). weights of shape \(1,\) was passed." @@ -467,46 +512,85 @@ def test_doubleml_exception_weights(): msg = rf"weights must have shape \({n},\). weights of shape \(1,\) was passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': np.ones(1), 'weights_bar': np.ones(1)}) + _ = DoubleMLIRM( + dml_data_irm, Lasso(), LogisticRegression(), weights={"weights": np.ones(1), "weights_bar": np.ones(1)} + ) msg = rf"weights must have shape \({n},\). weights of shape \({n}, 2\) was passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': np.ones((n, 2)), 'weights_bar': np.ones((n, 2))}) + _ = DoubleMLIRM( + dml_data_irm, Lasso(), LogisticRegression(), weights={"weights": np.ones((n, 2)), "weights_bar": np.ones((n, 2))} + ) msg = rf"weights_bar must have shape \({n}, 1\). weights_bar of shape \({n}, 2\) was passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': np.ones(n), 'weights_bar': np.ones((n, 2))}) + _ = DoubleMLIRM( + dml_data_irm, Lasso(), LogisticRegression(), weights={"weights": np.ones(n), "weights_bar": np.ones((n, 2))} + ) # value checks msg = "All weights values must be greater or equal 0." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights=-1*np.ones(n,)) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': -1*np.ones(n,), 'weights_bar': np.ones((n, 1))}) - with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': np.ones(n,), 'weights_bar': -1*np.ones((n, 1))}) + _ = DoubleMLIRM( + dml_data_irm, + Lasso(), + LogisticRegression(), + weights=-1 + * np.ones( + n, + ), + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLIRM( + dml_data_irm, + Lasso(), + LogisticRegression(), + weights={ + "weights": -1 + * np.ones( + n, + ), + "weights_bar": np.ones((n, 1)), + }, + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLIRM( + dml_data_irm, + Lasso(), + LogisticRegression(), + weights={ + "weights": np.ones( + n, + ), + "weights_bar": -1 * np.ones((n, 1)), + }, + ) msg = "At least one weight must be non-zero." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights=np.zeros((dml_data_irm.d.shape[0], ))) + _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), weights=np.zeros((dml_data_irm.d.shape[0],))) with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': np.zeros((dml_data_irm.d.shape[0], )), - 'weights_bar': np.ones((dml_data_irm.d.shape[0], 1))}) + _ = DoubleMLIRM( + dml_data_irm, + Lasso(), + LogisticRegression(), + weights={"weights": np.zeros((dml_data_irm.d.shape[0],)), "weights_bar": np.ones((dml_data_irm.d.shape[0], 1))}, + ) with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - weights={'weights': np.ones((dml_data_irm.d.shape[0], )), - 'weights_bar': np.zeros((dml_data_irm.d.shape[0], 1))}) + _ = DoubleMLIRM( + dml_data_irm, + Lasso(), + LogisticRegression(), + weights={"weights": np.ones((dml_data_irm.d.shape[0],)), "weights_bar": np.zeros((dml_data_irm.d.shape[0], 1))}, + ) msg = "weights must be binary for ATTE score." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - score='ATTE', weights=np.random.choice([0, 0.2], dml_data_irm.d.shape[0])) + _ = DoubleMLIRM( + dml_data_irm, + Lasso(), + LogisticRegression(), + score="ATTE", + weights=np.random.choice([0, 0.2], dml_data_irm.d.shape[0]), + ) @pytest.mark.ci @@ -521,13 +605,13 @@ def test_doubleml_exception_quantiles(): msg = "Quantile has be between 0 or 1. Quantile 1.0 passed." with pytest.raises(ValueError, match=msg): - _ = DoubleMLPQ(dml_data_irm, ml_g, ml_m, treatment=1, quantile=1.) + _ = DoubleMLPQ(dml_data_irm, ml_g, ml_m, treatment=1, quantile=1.0) with pytest.raises(ValueError, match=msg): - _ = DoubleMLLPQ(dml_data_iivm, ml_g, ml_m, treatment=1, quantile=1.) + _ = DoubleMLLPQ(dml_data_iivm, ml_g, ml_m, treatment=1, quantile=1.0) with pytest.raises(ValueError, match=msg): - _ = DoubleMLCVAR(dml_data_irm, ml_g, ml_m, treatment=1, quantile=1.) + _ = DoubleMLCVAR(dml_data_irm, ml_g, ml_m, treatment=1, quantile=1.0) - msg = r'Quantiles have be between 0 or 1. Quantiles \[0.2 2. \] passed.' + msg = r"Quantiles have be between 0 or 1. Quantiles \[0.2 2. \] passed." with pytest.raises(ValueError, match=msg): _ = DoubleMLQTE(dml_data_irm, ml_g, ml_m, quantiles=[0.2, 2]) @@ -588,32 +672,48 @@ def test_doubleml_exception_ipw_normalization(): @pytest.mark.ci def test_doubleml_exception_subgroups(): - msg = 'Invalid subgroups True. subgroups must be of type dictionary.' + msg = "Invalid subgroups True. subgroups must be of type dictionary." with pytest.raises(TypeError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - subgroups=True) + _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), subgroups=True) msg = "Invalid subgroups {'abs': True}. subgroups must be a dictionary with keys always_takers and never_takers." with pytest.raises(ValueError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - subgroups={'abs': True}) - msg = ("Invalid subgroups {'always_takers': True, 'never_takers': False, 'abs': 5}. " - "subgroups must be a dictionary with keys always_takers and never_takers.") - with pytest.raises(ValueError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - subgroups={'always_takers': True, 'never_takers': False, 'abs': 5}) - msg = ("Invalid subgroups {'always_takers': True}. " - "subgroups must be a dictionary with keys always_takers and never_takers.") - with pytest.raises(ValueError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - subgroups={'always_takers': True}) + _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), subgroups={"abs": True}) + msg = ( + "Invalid subgroups {'always_takers': True, 'never_takers': False, 'abs': 5}. " + "subgroups must be a dictionary with keys always_takers and never_takers." + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLIIVM( + dml_data_iivm, + Lasso(), + LogisticRegression(), + LogisticRegression(), + subgroups={"always_takers": True, "never_takers": False, "abs": 5}, + ) + msg = ( + "Invalid subgroups {'always_takers': True}. " + "subgroups must be a dictionary with keys always_takers and never_takers." + ) + with pytest.raises(ValueError, match=msg): + _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), subgroups={"always_takers": True}) msg = r"subgroups\['always_takers'\] must be True or False. Got 5." with pytest.raises(TypeError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - subgroups={'always_takers': 5, 'never_takers': False}) + _ = DoubleMLIIVM( + dml_data_iivm, + Lasso(), + LogisticRegression(), + LogisticRegression(), + subgroups={"always_takers": 5, "never_takers": False}, + ) msg = r"subgroups\['never_takers'\] must be True or False. Got 5." with pytest.raises(TypeError, match=msg): - _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - subgroups={'always_takers': True, 'never_takers': 5}) + _ = DoubleMLIIVM( + dml_data_iivm, + Lasso(), + LogisticRegression(), + LogisticRegression(), + subgroups={"always_takers": True, "never_takers": 5}, + ) @pytest.mark.ci @@ -621,45 +721,46 @@ def test_doubleml_exception_resampling(): msg = "The number of folds must be of int type. 1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=1.5) - msg = ('The number of repetitions for the sample splitting must be of int type. ' - "1.5 of type was passed.") + msg = "The number of repetitions for the sample splitting must be of int type. " "1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_rep=1.5) - msg = 'The number of folds must be positive. 0 was passed.' + msg = "The number of folds must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=0) - msg = 'The number of repetitions for the sample splitting must be positive. 0 was passed.' + msg = "The number of repetitions for the sample splitting must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_rep=0) - msg = 'draw_sample_splitting must be True or False. Got true.' + msg = "draw_sample_splitting must be True or False. Got true." with pytest.raises(TypeError, match=msg): - _ = DoubleMLPLR(dml_data, ml_l, ml_m, draw_sample_splitting='true') + _ = DoubleMLPLR(dml_data, ml_l, ml_m, draw_sample_splitting="true") @pytest.mark.ci def test_doubleml_exception_onefold(): - msg = 'n_folds must be greater than 1. You can use set_sample_splitting with a tuple to only use one fold.' + msg = "n_folds must be greater than 1. You can use set_sample_splitting with a tuple to only use one fold." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=1) @pytest.mark.ci def test_doubleml_exception_get_params(): - msg = 'Invalid nuisance learner ml_r. Valid nuisance learner ml_l or ml_m.' + msg = "Invalid nuisance learner ml_r. Valid nuisance learner ml_l or ml_m." with pytest.raises(ValueError, match=msg): - dml_plr.get_params('ml_r') - msg = 'Invalid nuisance learner ml_g. Valid nuisance learner ml_l or ml_m.' + dml_plr.get_params("ml_r") + msg = "Invalid nuisance learner ml_g. Valid nuisance learner ml_l or ml_m." with pytest.raises(ValueError, match=msg): - dml_plr.get_params('ml_g') - msg = 'Invalid nuisance learner ml_r. Valid nuisance learner ml_l or ml_m or ml_g.' + dml_plr.get_params("ml_g") + msg = "Invalid nuisance learner ml_r. Valid nuisance learner ml_l or ml_m or ml_g." with pytest.raises(ValueError, match=msg): - dml_plr_iv_type.get_params('ml_r') + dml_plr_iv_type.get_params("ml_r") @pytest.mark.ci def test_doubleml_exception_smpls(): - msg = ('Sample splitting not specified. ' - r'Either draw samples via .draw_sample splitting\(\) or set external samples via .set_sample_splitting\(\).') + msg = ( + "Sample splitting not specified. " + r"Either draw samples via .draw_sample splitting\(\) or set external samples via .set_sample_splitting\(\)." + ) dml_plr_no_smpls = DoubleMLPLR(dml_data, ml_l, ml_m, draw_sample_splitting=False) with pytest.raises(ValueError, match=msg): _ = dml_plr_no_smpls.smpls @@ -669,52 +770,44 @@ def test_doubleml_exception_smpls(): dml_pliv_cluster = DoubleMLPLIV(dml_cluster_data_pliv, ml_g, ml_m, ml_r) smpls = dml_plr.smpls - msg = ('For cluster data, all_smpls_cluster must be provided.') + msg = "For cluster data, all_smpls_cluster must be provided." with pytest.raises(ValueError, match=msg): _ = dml_pliv_cluster.set_sample_splitting(smpls) all_smpls_cluster = copy.deepcopy(dml_pliv_cluster.smpls_cluster) all_smpls_cluster.append(all_smpls_cluster[0]) - msg = ('Invalid samples provided. Number of repetitions for all_smpls and all_smpls_cluster must be the same.') + msg = "Invalid samples provided. Number of repetitions for all_smpls and all_smpls_cluster must be the same." with pytest.raises(ValueError, match=msg): - _ = dml_pliv_cluster.set_sample_splitting( - all_smpls=dml_pliv_cluster.smpls, - all_smpls_cluster=all_smpls_cluster) + _ = dml_pliv_cluster.set_sample_splitting(all_smpls=dml_pliv_cluster.smpls, all_smpls_cluster=all_smpls_cluster) all_smpls_cluster = copy.deepcopy(dml_pliv_cluster.smpls_cluster) all_smpls_cluster[0] = all_smpls_cluster[0][0] - msg = ('Invalid samples provided. Number of folds for all_smpls and all_smpls_cluster must be the same.') + msg = "Invalid samples provided. Number of folds for all_smpls and all_smpls_cluster must be the same." with pytest.raises(ValueError, match=msg): - _ = dml_pliv_cluster.set_sample_splitting( - all_smpls=dml_pliv_cluster.smpls, - all_smpls_cluster=all_smpls_cluster) + _ = dml_pliv_cluster.set_sample_splitting(all_smpls=dml_pliv_cluster.smpls, all_smpls_cluster=all_smpls_cluster) all_smpls_cluster = copy.deepcopy(dml_pliv_cluster.smpls_cluster) all_smpls_cluster[0][0][1][1] = np.append(all_smpls_cluster[0][0][1][1], [11], axis=0) - msg = ('Invalid cluster partition provided. At least one inner list does not form a partition.') + msg = "Invalid cluster partition provided. At least one inner list does not form a partition." with pytest.raises(ValueError, match=msg): - _ = dml_pliv_cluster.set_sample_splitting( - all_smpls=dml_pliv_cluster.smpls, - all_smpls_cluster=all_smpls_cluster) + _ = dml_pliv_cluster.set_sample_splitting(all_smpls=dml_pliv_cluster.smpls, all_smpls_cluster=all_smpls_cluster) all_smpls_cluster = copy.deepcopy(dml_pliv_cluster.smpls_cluster) all_smpls_cluster[0][0][1][1][1] = 11 - msg = ('Invalid cluster partition provided. At least one inner list does not form a partition.') + msg = "Invalid cluster partition provided. At least one inner list does not form a partition." with pytest.raises(ValueError, match=msg): - _ = dml_pliv_cluster.set_sample_splitting( - all_smpls=dml_pliv_cluster.smpls, - all_smpls_cluster=all_smpls_cluster) + _ = dml_pliv_cluster.set_sample_splitting(all_smpls=dml_pliv_cluster.smpls, all_smpls_cluster=all_smpls_cluster) @pytest.mark.ci def test_doubleml_exception_fit(): msg = "The number of CPUs used to fit the learners must be of int type. 5 of type was passed." with pytest.raises(TypeError, match=msg): - dml_plr.fit(n_jobs_cv='5') - msg = 'store_predictions must be True or False. Got 1.' + dml_plr.fit(n_jobs_cv="5") + msg = "store_predictions must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_plr.fit(store_predictions=1) - msg = 'store_models must be True or False. Got 1.' + msg = "store_models must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_plr.fit(store_models=1) @@ -722,18 +815,18 @@ def test_doubleml_exception_fit(): @pytest.mark.ci def test_doubleml_exception_bootstrap(): dml_plr_boot = DoubleMLPLR(dml_data, ml_l, ml_m) - msg = r'Apply fit\(\) before bootstrap\(\).' + msg = r"Apply fit\(\) before bootstrap\(\)." with pytest.raises(ValueError, match=msg): dml_plr_boot.bootstrap() dml_plr_boot.fit() msg = 'Method must be "Bayes", "normal" or "wild". Got Gaussian.' with pytest.raises(ValueError, match=msg): - dml_plr_boot.bootstrap(method='Gaussian') + dml_plr_boot.bootstrap(method="Gaussian") msg = "The number of bootstrap replications must be of int type. 500 of type was passed." with pytest.raises(TypeError, match=msg): - dml_plr_boot.bootstrap(n_rep_boot='500') - msg = 'The number of bootstrap replications must be positive. 0 was passed.' + dml_plr_boot.bootstrap(n_rep_boot="500") + msg = "The number of bootstrap replications must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): dml_plr_boot.bootstrap(n_rep_boot=0) @@ -743,21 +836,21 @@ def test_doubleml_exception_confint(): dml_plr_confint = DoubleMLPLR(dml_data, ml_l, ml_m) dml_plr_confint.fit() - msg = 'joint must be True or False. Got 1.' + msg = "joint must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_plr_confint.confint(joint=1) msg = "The confidence level must be of float type. 5% of type was passed." with pytest.raises(TypeError, match=msg): - dml_plr_confint.confint(level='5%') - msg = r'The confidence level must be in \(0,1\). 0.0 was passed.' + dml_plr_confint.confint(level="5%") + msg = r"The confidence level must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): - dml_plr_confint.confint(level=0.) + dml_plr_confint.confint(level=0.0) dml_plr_confint_not_fitted = DoubleMLPLR(dml_data, ml_l, ml_m) - msg = r'Apply fit\(\) before confint\(\).' + msg = r"Apply fit\(\) before confint\(\)." with pytest.raises(ValueError, match=msg): dml_plr_confint_not_fitted.confint() - msg = r'Apply bootstrap\(\) before confint\(joint=True\).' + msg = r"Apply bootstrap\(\) before confint\(joint=True\)." with pytest.raises(ValueError, match=msg): dml_plr_confint.confint(joint=True) dml_plr_confint.bootstrap() @@ -769,15 +862,15 @@ def test_doubleml_exception_confint(): def test_doubleml_exception_p_adjust(): dml_plr_p_adjust = DoubleMLPLR(dml_data, ml_l, ml_m) - msg = r'Apply fit\(\) before p_adjust\(\).' + msg = r"Apply fit\(\) before p_adjust\(\)." with pytest.raises(ValueError, match=msg): dml_plr_p_adjust.p_adjust() dml_plr_p_adjust.fit() msg = r'Apply bootstrap\(\) before p_adjust\("romano-wolf"\).' with pytest.raises(ValueError, match=msg): - dml_plr_p_adjust.p_adjust(method='romano-wolf') + dml_plr_p_adjust.p_adjust(method="romano-wolf") dml_plr_p_adjust.bootstrap() - p_val = dml_plr_p_adjust.p_adjust(method='romano-wolf') + p_val = dml_plr_p_adjust.p_adjust(method="romano-wolf") assert isinstance(p_val, pd.DataFrame) msg = "The p_adjust method must be of str type. 0.05 of type was passed." @@ -787,74 +880,80 @@ def test_doubleml_exception_p_adjust(): @pytest.mark.ci def test_doubleml_exception_tune(): - msg = r'Invalid param_grids \[0.05, 0.5\]. param_grids must be a dictionary with keys ml_l and ml_m' + msg = r"Invalid param_grids \[0.05, 0.5\]. param_grids must be a dictionary with keys ml_l and ml_m" with pytest.raises(ValueError, match=msg): dml_plr.tune([0.05, 0.5]) - msg = (r"Invalid param_grids {'ml_r': {'alpha': \[0.05, 0.5\]}}. " - "param_grids must be a dictionary with keys ml_l and ml_m.") + msg = ( + r"Invalid param_grids {'ml_r': {'alpha': \[0.05, 0.5\]}}. " "param_grids must be a dictionary with keys ml_l and ml_m." + ) with pytest.raises(ValueError, match=msg): - dml_plr.tune({'ml_r': {'alpha': [0.05, 0.5]}}) + dml_plr.tune({"ml_r": {"alpha": [0.05, 0.5]}}) - msg = r'Invalid param_grids \[0.05, 0.5\]. param_grids must be a dictionary with keys ml_l and ml_m and ml_g' + msg = r"Invalid param_grids \[0.05, 0.5\]. param_grids must be a dictionary with keys ml_l and ml_m and ml_g" with pytest.raises(ValueError, match=msg): dml_plr_iv_type.tune([0.05, 0.5]) - msg = (r"Invalid param_grids {'ml_g': {'alpha': \[0.05, 0.5\]}, 'ml_m': {'alpha': \[0.05, 0.5\]}}. " - "param_grids must be a dictionary with keys ml_l and ml_m and ml_g.") + msg = ( + r"Invalid param_grids {'ml_g': {'alpha': \[0.05, 0.5\]}, 'ml_m': {'alpha': \[0.05, 0.5\]}}. " + "param_grids must be a dictionary with keys ml_l and ml_m and ml_g." + ) with pytest.raises(ValueError, match=msg): - dml_plr_iv_type.tune({'ml_g': {'alpha': [0.05, 0.5]}, - 'ml_m': {'alpha': [0.05, 0.5]}}) + dml_plr_iv_type.tune({"ml_g": {"alpha": [0.05, 0.5]}, "ml_m": {"alpha": [0.05, 0.5]}}) - param_grids = {'ml_l': {'alpha': [0.05, 0.5]}, 'ml_m': {'alpha': [0.05, 0.5]}} - msg = ('Invalid scoring_methods neg_mean_absolute_error. ' - 'scoring_methods must be a dictionary. ' - 'Valid keys are ml_l and ml_m.') + param_grids = {"ml_l": {"alpha": [0.05, 0.5]}, "ml_m": {"alpha": [0.05, 0.5]}} + msg = ( + "Invalid scoring_methods neg_mean_absolute_error. " + "scoring_methods must be a dictionary. " + "Valid keys are ml_l and ml_m." + ) with pytest.raises(ValueError, match=msg): - dml_plr.tune(param_grids, scoring_methods='neg_mean_absolute_error') + dml_plr.tune(param_grids, scoring_methods="neg_mean_absolute_error") - msg = 'tune_on_folds must be True or False. Got 1.' + msg = "tune_on_folds must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_plr.tune(param_grids, tune_on_folds=1) - msg = 'The number of folds used for tuning must be at least two. 1 was passed.' + msg = "The number of folds used for tuning must be at least two. 1 was passed." with pytest.raises(ValueError, match=msg): dml_plr.tune(param_grids, n_folds_tune=1) msg = "The number of folds used for tuning must be of int type. 1.0 of type was passed." with pytest.raises(TypeError, match=msg): - dml_plr.tune(param_grids, n_folds_tune=1.) + dml_plr.tune(param_grids, n_folds_tune=1.0) msg = 'search_mode must be "grid_search" or "randomized_search". Got gridsearch.' with pytest.raises(ValueError, match=msg): - dml_plr.tune(param_grids, search_mode='gridsearch') + dml_plr.tune(param_grids, search_mode="gridsearch") - msg = 'The number of parameter settings sampled for the randomized search must be at least two. 1 was passed.' + msg = "The number of parameter settings sampled for the randomized search must be at least two. 1 was passed." with pytest.raises(ValueError, match=msg): dml_plr.tune(param_grids, n_iter_randomized_search=1) - msg = ("The number of parameter settings sampled for the randomized search must be of int type. " - "1.0 of type was passed.") + msg = ( + "The number of parameter settings sampled for the randomized search must be of int type. " + "1.0 of type was passed." + ) with pytest.raises(TypeError, match=msg): - dml_plr.tune(param_grids, n_iter_randomized_search=1.) + dml_plr.tune(param_grids, n_iter_randomized_search=1.0) msg = "The number of CPUs used to fit the learners must be of int type. 5 of type was passed." with pytest.raises(TypeError, match=msg): - dml_plr.tune(param_grids, n_jobs_cv='5') + dml_plr.tune(param_grids, n_jobs_cv="5") - msg = 'set_as_params must be True or False. Got 1.' + msg = "set_as_params must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_plr.tune(param_grids, set_as_params=1) - msg = 'return_tune_res must be True or False. Got 1.' + msg = "return_tune_res must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_plr.tune(param_grids, return_tune_res=1) @pytest.mark.ci def test_doubleml_exception_set_ml_nuisance_params(): - msg = 'Invalid nuisance learner g. Valid nuisance learner ml_l or ml_m.' + msg = "Invalid nuisance learner g. Valid nuisance learner ml_l or ml_m." with pytest.raises(ValueError, match=msg): - dml_plr.set_ml_nuisance_params('g', 'd', {'alpha': 0.1}) - msg = 'Invalid treatment variable y. Valid treatment variable d.' + dml_plr.set_ml_nuisance_params("g", "d", {"alpha": 0.1}) + msg = "Invalid treatment variable y. Valid treatment variable d." with pytest.raises(ValueError, match=msg): - dml_plr.set_ml_nuisance_params('ml_l', 'y', {'alpha': 0.1}) + dml_plr.set_ml_nuisance_params("ml_l", "y", {"alpha": 0.1}) class _DummyNoSetParams: @@ -886,13 +985,13 @@ def predict(self, X): @pytest.mark.ci def test_doubleml_exception_learner(): - err_msg_prefix = 'Invalid learner provided for ml_l: ' - warn_msg_prefix = 'Learner provided for ml_l is probably invalid: ' + err_msg_prefix = "Invalid learner provided for ml_l: " + warn_msg_prefix = "Learner provided for ml_l is probably invalid: " - msg = err_msg_prefix + 'provide an instance of a learner instead of a class.' + msg = err_msg_prefix + "provide an instance of a learner instead of a class." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, Lasso, ml_m) - msg = err_msg_prefix + r'BaseEstimator\(\) has no method .fit\(\).' + msg = err_msg_prefix + r"BaseEstimator\(\) has no method .fit\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, BaseEstimator(), ml_m) # msg = err_msg_prefix + r'_DummyNoSetParams\(\) has no method .set_params\(\).' @@ -908,54 +1007,63 @@ def test_doubleml_exception_learner(): # ToDo: Currently for ml_l (and others) we only check whether the learner can be identified as regressor. However, # we do not check whether it can instead be identified as classifier, which could be used to throw an error. - msg = warn_msg_prefix + r'LogisticRegression\(\) is \(probably\) no regressor.' + msg = warn_msg_prefix + r"LogisticRegression\(\) is \(probably\) no regressor." with pytest.warns(UserWarning, match=msg): _ = DoubleMLPLR(dml_data, LogisticRegression(), Lasso()) # we allow classifiers for ml_m in PLR, but only for binary treatment variables - msg = (r'The ml_m learner LogisticRegression\(\) was identified as classifier ' - 'but at least one treatment variable is not binary with values 0 and 1.') + msg = ( + r"The ml_m learner LogisticRegression\(\) was identified as classifier " + "but at least one treatment variable is not binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, Lasso(), LogisticRegression()) msg = r"For score = 'IV-type', learners ml_l and ml_g should be specified. Set ml_g = clone\(ml_l\)." with pytest.warns(UserWarning, match=msg): - _ = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=ml_m, score='IV-type') + _ = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=ml_m, score="IV-type") msg = 'A learner ml_g has been provided for score = "partialling out" but will be ignored.' with pytest.warns(UserWarning, match=msg): - _ = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=Lasso(), ml_g=Lasso(), score='partialling out') + _ = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=Lasso(), ml_g=Lasso(), score="partialling out") msg = "For score = 'IV-type', learners ml_l, ml_m, ml_r and ml_g need to be specified." with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLIV(dml_data_pliv, ml_l=ml_l, ml_m=ml_m, ml_r=ml_r, - score='IV-type') + _ = DoubleMLPLIV(dml_data_pliv, ml_l=ml_l, ml_m=ml_m, ml_r=ml_r, score="IV-type") msg = 'A learner ml_g has been provided for score = "partialling out" but will be ignored.' with pytest.warns(UserWarning, match=msg): - _ = DoubleMLPLIV(dml_data_pliv, ml_l=Lasso(), ml_m=Lasso(), ml_r=Lasso(), ml_g=Lasso(), score='partialling out') + _ = DoubleMLPLIV(dml_data_pliv, ml_l=Lasso(), ml_m=Lasso(), ml_r=Lasso(), ml_g=Lasso(), score="partialling out") # we allow classifiers for ml_g for binary treatment variables in IRM - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLIRM(dml_data_irm, LogisticRegression(), LogisticRegression()) # we allow classifiers for ml_g for binary treatment variables in IRM - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLIIVM(dml_data_iivm, LogisticRegression(), LogisticRegression(), LogisticRegression()) # we allow classifiers for ml_g for binary treatment variables in DID - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLDID(dml_data_did, LogisticRegression(), LogisticRegression()) # we allow classifiers for ml_g for binary treatment variables in DIDCS - msg = (r'The ml_g learner LogisticRegression\(\) was identified as classifier ' - 'but the outcome variable is not binary with values 0 and 1.') + msg = ( + r"The ml_g learner LogisticRegression\(\) was identified as classifier " + "but the outcome variable is not binary with values 0 and 1." + ) with pytest.raises(ValueError, match=msg): _ = DoubleMLDIDCS(dml_data_did_cs, LogisticRegression(), LogisticRegression()) @@ -963,13 +1071,17 @@ def test_doubleml_exception_learner(): # it then predicts labels and therefore an exception will be thrown log_reg = LogisticRegression() log_reg._estimator_type = None - msg = (r'Learner provided for ml_m is probably invalid: LogisticRegression\(\) is \(probably\) neither a regressor ' - 'nor a classifier. Method predict is used for prediction.') + msg = ( + r"Learner provided for ml_m is probably invalid: LogisticRegression\(\) is \(probably\) neither a regressor " + "nor a classifier. Method predict is used for prediction." + ) with pytest.warns(UserWarning, match=msg): dml_plr_hidden_classifier = DoubleMLPLR(dml_data_irm, Lasso(), log_reg) - msg = (r'For the binary variable d, predictions obtained with the ml_m learner LogisticRegression\(\) ' - 'are also observed to be binary with values 0 and 1. Make sure that for classifiers probabilities and not ' - 'labels are predicted.') + msg = ( + r"For the binary variable d, predictions obtained with the ml_m learner LogisticRegression\(\) " + "are also observed to be binary with values 0 and 1. Make sure that for classifiers probabilities and not " + "labels are predicted." + ) with pytest.raises(ValueError, match=msg): dml_plr_hidden_classifier.fit() @@ -978,32 +1090,40 @@ def test_doubleml_exception_learner(): # whether predict() or predict_proba() is being called can also be manipulated via the unrelated max_iter variable log_reg = LogisticRegressionManipulatedPredict() log_reg._estimator_type = None - msg = (r'Learner provided for ml_g is probably invalid: LogisticRegressionManipulatedPredict\(\) is \(probably\) ' - 'neither a regressor nor a classifier. Method predict is used for prediction.') + msg = ( + r"Learner provided for ml_g is probably invalid: LogisticRegressionManipulatedPredict\(\) is \(probably\) " + "neither a regressor nor a classifier. Method predict is used for prediction." + ) with pytest.warns(UserWarning, match=msg): - dml_irm_hidden_classifier = DoubleMLIRM(dml_data_irm_binary_outcome, - log_reg, LogisticRegression()) - msg = (r'For the binary variable y, predictions obtained with the ml_g learner ' - r'LogisticRegressionManipulatedPredict\(\) are also observed to be binary with values 0 and 1. Make sure ' - 'that for classifiers probabilities and not labels are predicted.') + dml_irm_hidden_classifier = DoubleMLIRM(dml_data_irm_binary_outcome, log_reg, LogisticRegression()) + msg = ( + r"For the binary variable y, predictions obtained with the ml_g learner " + r"LogisticRegressionManipulatedPredict\(\) are also observed to be binary with values 0 and 1. Make sure " + "that for classifiers probabilities and not labels are predicted." + ) with pytest.raises(ValueError, match=msg): dml_irm_hidden_classifier.fit() with pytest.raises(ValueError, match=msg): - dml_irm_hidden_classifier.set_ml_nuisance_params('ml_g0', 'd', {'max_iter': 314}) + dml_irm_hidden_classifier.set_ml_nuisance_params("ml_g0", "d", {"max_iter": 314}) dml_irm_hidden_classifier.fit() - msg = (r'Learner provided for ml_g is probably invalid: LogisticRegressionManipulatedPredict\(\) is \(probably\) ' - 'neither a regressor nor a classifier. Method predict is used for prediction.') + msg = ( + r"Learner provided for ml_g is probably invalid: LogisticRegressionManipulatedPredict\(\) is \(probably\) " + "neither a regressor nor a classifier. Method predict is used for prediction." + ) with pytest.warns(UserWarning, match=msg): - dml_iivm_hidden_classifier = DoubleMLIIVM(dml_data_iivm_binary_outcome, - log_reg, LogisticRegression(), LogisticRegression()) - msg = (r'For the binary variable y, predictions obtained with the ml_g learner ' - r'LogisticRegressionManipulatedPredict\(\) are also observed to be binary with values 0 and 1. Make sure ' - 'that for classifiers probabilities and not labels are predicted.') + dml_iivm_hidden_classifier = DoubleMLIIVM( + dml_data_iivm_binary_outcome, log_reg, LogisticRegression(), LogisticRegression() + ) + msg = ( + r"For the binary variable y, predictions obtained with the ml_g learner " + r"LogisticRegressionManipulatedPredict\(\) are also observed to be binary with values 0 and 1. Make sure " + "that for classifiers probabilities and not labels are predicted." + ) with pytest.raises(ValueError, match=msg): dml_iivm_hidden_classifier.fit() with pytest.raises(ValueError, match=msg): - dml_iivm_hidden_classifier.set_ml_nuisance_params('ml_g0', 'd', {'max_iter': 314}) + dml_iivm_hidden_classifier.set_ml_nuisance_params("ml_g0", "d", {"max_iter": 314}) dml_iivm_hidden_classifier.fit() @@ -1013,7 +1133,7 @@ def test_doubleml_exception_and_warning_learner(): # msg = err_msg_prefix + r'_DummyNoClassifier\(\) has no method .predict\(\).' with pytest.raises(TypeError): _ = DoubleMLPLR(dml_data, _DummyNoClassifier(), Lasso()) - msg = 'Invalid learner provided for ml_m: ' + r'Lasso\(\) has no method .predict_proba\(\).' + msg = "Invalid learner provided for ml_m: " + r"Lasso\(\) has no method .predict_proba\(\)." with pytest.raises(TypeError, match=msg): _ = DoubleMLIRM(dml_data_irm, Lasso(), Lasso()) @@ -1025,11 +1145,11 @@ def test_doubleml_sensitivity_not_yet_implemented(): dml_pliv = DoubleMLPLIV(dml_data_pliv, ml_g, ml_m, ml_r) dml_pliv.fit() - msg = 'Sensitivity analysis is not implemented for this model.' + msg = "Sensitivity analysis is not implemented for this model." with pytest.raises(NotImplementedError, match=msg): _ = dml_pliv.sensitivity_analysis() - msg = 'Sensitivity analysis not yet implemented for DoubleMLPLIV.' + msg = "Sensitivity analysis not yet implemented for DoubleMLPLIV." with pytest.raises(NotImplementedError, match=msg): _ = dml_pliv.sensitivity_benchmark(benchmarking_set=["X1"]) @@ -1044,7 +1164,7 @@ def test_doubleml_sensitivity_inputs(): with pytest.raises(TypeError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=1) - msg = r'cf_y must be in \[0,1\). 1.0 was passed.' + msg = r"cf_y must be in \[0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=1.0) @@ -1053,7 +1173,7 @@ def test_doubleml_sensitivity_inputs(): with pytest.raises(TypeError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=1) - msg = r'cf_d must be in \[0,1\). 1.0 was passed.' + msg = r"cf_d must be in \[0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=1.0) @@ -1066,7 +1186,7 @@ def test_doubleml_sensitivity_inputs(): with pytest.raises(TypeError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho="1") - msg = r'The absolute value of rho must be in \[0,1\]. 1.1 was passed.' + msg = r"The absolute value of rho must be in \[0,1\]. 1.1 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.1) @@ -1075,11 +1195,11 @@ def test_doubleml_sensitivity_inputs(): with pytest.raises(TypeError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.0, level=1) - msg = r'The confidence level must be in \(0,1\). 1.0 was passed.' + msg = r"The confidence level must be in \(0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.0, level=1.0) - msg = r'The confidence level must be in \(0,1\). 0.0 was passed.' + msg = r"The confidence level must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.0, level=0.0) @@ -1106,28 +1226,29 @@ def test_doubleml_sensitivity_inputs(): _ = dml_irm.sensitivity_plot(idx_treatment=1) # test setter - msg = ("_sensitivity_element_est must return sensitivity elements in a dict. " - "Got type .") + msg = "_sensitivity_element_est must return sensitivity elements in a dict. " "Got type ." with pytest.raises(TypeError, match=msg): _ = dml_irm._set_sensitivity_elements(sensitivity_elements=1, i_rep=0, i_treat=0) - sensitivity_elements = dict({'sigma2': 1}) + sensitivity_elements = dict({"sigma2": 1}) with pytest.raises(ValueError): _ = dml_irm._set_sensitivity_elements(sensitivity_elements=sensitivity_elements, i_rep=0, i_treat=0) # test variances - sensitivity_elements = dict({'sigma2': 1.0, 'nu2': -2.4, 'psi_sigma2': 1.0, 'psi_nu2': 1.0, 'riesz_rep': 1.0}) + sensitivity_elements = dict({"sigma2": 1.0, "nu2": -2.4, "psi_sigma2": 1.0, "psi_nu2": 1.0, "riesz_rep": 1.0}) _ = dml_irm._set_sensitivity_elements(sensitivity_elements=sensitivity_elements, i_rep=0, i_treat=0) - msg = ('sensitivity_elements sigma2 and nu2 have to be positive. ' - r'Got sigma2 \[\[\[1.\]\]\] and nu2 \[\[\[-2.4\]\]\]. ' - r'Most likely this is due to low quality learners \(especially propensity scores\).') + msg = ( + "sensitivity_elements sigma2 and nu2 have to be positive. " + r"Got sigma2 \[\[\[1.\]\]\] and nu2 \[\[\[-2.4\]\]\]. " + r"Most likely this is due to low quality learners \(especially propensity scores\)." + ) with pytest.raises(ValueError, match=msg): dml_irm.sensitivity_analysis() def test_doubleml_sensitivity_summary(): dml_irm = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), trimming_threshold=0.1) - msg = r'Apply sensitivity_analysis\(\) before sensitivity_summary.' + msg = r"Apply sensitivity_analysis\(\) before sensitivity_summary." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_summary @@ -1146,10 +1267,12 @@ def test_doubleml_sensitivity_benchmark(): with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_benchmark(benchmarking_set=[]) - msg = (r"benchmarking_set must be a subset of features \['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', " - r"'X11', 'X12', 'X13', 'X14', 'X15', 'X16', 'X17', 'X18', 'X19', 'X20'\]. \['test_var'\] was passed.") + msg = ( + r"benchmarking_set must be a subset of features \['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', " + r"'X11', 'X12', 'X13', 'X14', 'X15', 'X16', 'X17', 'X18', 'X19', 'X20'\]. \['test_var'\] was passed." + ) with pytest.raises(ValueError, match=msg): - _ = dml_irm.sensitivity_benchmark(benchmarking_set=['test_var']) + _ = dml_irm.sensitivity_benchmark(benchmarking_set=["test_var"]) @pytest.mark.ci @@ -1157,7 +1280,7 @@ def test_doubleml_sensitivity_plot_input(): dml_irm = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), trimming_threshold=0.1) dml_irm.fit() - msg = (r'Apply sensitivity_analysis\(\) to include senario in sensitivity_plot. ') + msg = r"Apply sensitivity_analysis\(\) to include senario in sensitivity_plot. " with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_plot() @@ -1171,26 +1294,26 @@ def test_doubleml_sensitivity_plot_input(): _ = dml_irm.sensitivity_plot(benchmarks="True") msg = r"benchmarks has to be a dictionary with keys cf_y, cf_d and name. Got dict_keys\(\['cf_y', 'cf_d'\]\)." with pytest.raises(ValueError, match=msg): - _ = dml_irm.sensitivity_plot(benchmarks={'cf_y': 0.1, 'cf_d': 0.15}) + _ = dml_irm.sensitivity_plot(benchmarks={"cf_y": 0.1, "cf_d": 0.15}) msg = r"benchmarks has to be a dictionary with values of same length. Got \[1, 2, 2\]." with pytest.raises(ValueError, match=msg): - _ = dml_irm.sensitivity_plot(benchmarks={'cf_y': [0.1], 'cf_d': [0.15, 0.2], 'name': ['test', 'test2']}) + _ = dml_irm.sensitivity_plot(benchmarks={"cf_y": [0.1], "cf_d": [0.15, 0.2], "name": ["test", "test2"]}) msg = "benchmarks cf_y must be of float type. 2 of type was passed." with pytest.raises(TypeError, match=msg): - _ = dml_irm.sensitivity_plot(benchmarks={'cf_y': [0.1, 2], 'cf_d': [0.15, 0.2], 'name': ['test', 'test2']}) - msg = r'benchmarks cf_y must be in \[0,1\). 1.0 was passed.' + _ = dml_irm.sensitivity_plot(benchmarks={"cf_y": [0.1, 2], "cf_d": [0.15, 0.2], "name": ["test", "test2"]}) + msg = r"benchmarks cf_y must be in \[0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): - _ = dml_irm.sensitivity_plot(benchmarks={'cf_y': [0.1, 1.0], 'cf_d': [0.15, 0.2], 'name': ['test', 'test2']}) + _ = dml_irm.sensitivity_plot(benchmarks={"cf_y": [0.1, 1.0], "cf_d": [0.15, 0.2], "name": ["test", "test2"]}) msg = "benchmarks name must be of string type. 2 of type was passed." with pytest.raises(TypeError, match=msg): - _ = dml_irm.sensitivity_plot(benchmarks={'cf_y': [0.1, 0.2], 'cf_d': [0.15, 0.2], 'name': [2, 2]}) + _ = dml_irm.sensitivity_plot(benchmarks={"cf_y": [0.1, 0.2], "cf_d": [0.15, 0.2], "name": [2, 2]}) msg = "value must be a string. 2 of type was passed." with pytest.raises(TypeError, match=msg): _ = dml_irm.sensitivity_plot(value=2) msg = "Invalid value test. Valid values theta or ci." with pytest.raises(ValueError, match=msg): - _ = dml_irm.sensitivity_plot(value='test') + _ = dml_irm.sensitivity_plot(value="test") msg = "fill has to be boolean. True of type was passed." with pytest.raises(TypeError, match=msg): @@ -1208,12 +1331,12 @@ def test_doubleml_sensitivity_plot_input(): _ = dml_irm.sensitivity_plot(grid_bounds=(0.15, 1)) with pytest.raises(TypeError, match=msg): _ = dml_irm.sensitivity_plot(grid_bounds=(1, 0.15)) - msg = r'grid_bounds must be in \(0,1\). 1.0 was passed.' + msg = r"grid_bounds must be in \(0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_plot(grid_bounds=(1.0, 0.15)) with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_plot(grid_bounds=(0.15, 1.0)) - msg = r'grid_bounds must be in \(0,1\). 0.0 was passed.' + msg = r"grid_bounds must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_irm.sensitivity_plot(grid_bounds=(0.0, 0.15)) with pytest.raises(ValueError, match=msg): @@ -1224,16 +1347,22 @@ def test_doubleml_sensitivity_plot_input(): def test_doubleml_cluster_not_yet_implemented(): dml_pliv_cluster = DoubleMLPLIV(dml_cluster_data_pliv, ml_g, ml_m, ml_r) dml_pliv_cluster.fit() - msg = 'bootstrap not yet implemented with clustering.' + msg = "bootstrap not yet implemented with clustering." with pytest.raises(NotImplementedError, match=msg): _ = dml_pliv_cluster.bootstrap() df = dml_cluster_data_pliv.data.copy() - df['cluster_var_k'] = df['cluster_var_i'] + df['cluster_var_j'] - 2 - dml_cluster_data_multiway = DoubleMLClusterData(df, y_col='Y', d_cols='D', x_cols=['X1', 'X5'], z_cols='Z', - cluster_cols=['cluster_var_i', 'cluster_var_j', 'cluster_var_k']) + df["cluster_var_k"] = df["cluster_var_i"] + df["cluster_var_j"] - 2 + dml_cluster_data_multiway = DoubleMLClusterData( + df, + y_col="Y", + d_cols="D", + x_cols=["X1", "X5"], + z_cols="Z", + cluster_cols=["cluster_var_i", "cluster_var_j", "cluster_var_k"], + ) assert dml_cluster_data_multiway.n_cluster_vars == 3 - msg = r'Multi-way \(n_ways > 2\) clustering not yet implemented.' + msg = r"Multi-way \(n_ways > 2\) clustering not yet implemented." with pytest.raises(NotImplementedError, match=msg): _ = DoubleMLPLIV(dml_cluster_data_multiway, ml_g, ml_m, ml_r) @@ -1256,17 +1385,17 @@ def predict(self, X): @pytest.mark.ci def test_doubleml_nan_prediction(): - msg = r'Predictions from learner LassoWithNanPred\(\) for ml_l are not finite.' + msg = r"Predictions from learner LassoWithNanPred\(\) for ml_l are not finite." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, LassoWithNanPred(), ml_m).fit() - msg = r'Predictions from learner LassoWithInfPred\(\) for ml_l are not finite.' + msg = r"Predictions from learner LassoWithInfPred\(\) for ml_l are not finite." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, LassoWithInfPred(), ml_m).fit() - msg = r'Predictions from learner LassoWithNanPred\(\) for ml_m are not finite.' + msg = r"Predictions from learner LassoWithNanPred\(\) for ml_m are not finite." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, LassoWithNanPred()).fit() - msg = r'Predictions from learner LassoWithInfPred\(\) for ml_m are not finite.' + msg = r"Predictions from learner LassoWithInfPred\(\) for ml_m are not finite." with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, LassoWithInfPred()).fit() @@ -1276,145 +1405,114 @@ def test_doubleml_warning_blp(): n = 5 np.random.seed(42) random_basis = pd.DataFrame(np.random.normal(0, 1, size=(n, 3))) - random_signal = np.random.normal(0, 1, size=(n, )) + random_signal = np.random.normal(0, 1, size=(n,)) blp = DoubleMLBLP(random_signal, random_basis) blp.fit() - msg = 'Returning pointwise confidence intervals for basis coefficients.' + msg = "Returning pointwise confidence intervals for basis coefficients." with pytest.warns(UserWarning, match=msg): _ = blp.confint(joint=True) @pytest.mark.ci def test_doubleml_exception_gate(): - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5) + dml_irm_obj = DoubleMLIRM(dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5) dml_irm_obj.fit() msg = "Groups must be of DataFrame type. Groups of type was passed." with pytest.raises(TypeError, match=msg): dml_irm_obj.gate(groups=2) groups = pd.DataFrame(np.random.normal(0, 1, size=(dml_data_irm.n_obs, 3))) - msg = (r'Columns of groups must be of bool type or int type \(dummy coded\). ' - 'Alternatively, groups should only contain one column.') + msg = ( + r"Columns of groups must be of bool type or int type \(dummy coded\). " + "Alternatively, groups should only contain one column." + ) with pytest.raises(TypeError, match=msg): dml_irm_obj.gate(groups=groups) - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATTE') + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATTE" + ) dml_irm_obj.fit() groups = pd.DataFrame(np.random.choice([True, False], size=dml_data_irm.n_obs)) - msg = 'Invalid score ATTE. Valid score ATE.' + msg = "Invalid score ATTE. Valid score ATE." with pytest.raises(ValueError, match=msg): dml_irm_obj.gate(groups=groups) - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATE', - n_rep=2) + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATE", n_rep=2 + ) dml_irm_obj.fit() - msg = 'Only implemented for one repetition. Number of repetitions is 2.' + msg = "Only implemented for one repetition. Number of repetitions is 2." with pytest.raises(NotImplementedError, match=msg): dml_irm_obj.gate(groups=groups) @pytest.mark.ci def test_doubleml_exception_cate(): - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATTE') + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATTE" + ) dml_irm_obj.fit() - msg = 'Invalid score ATTE. Valid score ATE.' + msg = "Invalid score ATTE. Valid score ATE." with pytest.raises(ValueError, match=msg): dml_irm_obj.cate(basis=2) - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATE', - n_rep=2) + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATE", n_rep=2 + ) dml_irm_obj.fit() - msg = 'Only implemented for one repetition. Number of repetitions is 2.' + msg = "Only implemented for one repetition. Number of repetitions is 2." with pytest.raises(NotImplementedError, match=msg): dml_irm_obj.cate(basis=2) @pytest.mark.ci def test_doubleml_exception_plr_cate(): - dml_plr_obj = DoubleMLPLR(dml_data, - ml_l=Lasso(), - ml_m=Lasso(), - n_folds=2, - n_rep=2) + dml_plr_obj = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=Lasso(), n_folds=2, n_rep=2) dml_plr_obj.fit() - msg = 'Only implemented for one repetition. Number of repetitions is 2.' + msg = "Only implemented for one repetition. Number of repetitions is 2." with pytest.raises(NotImplementedError, match=msg): dml_plr_obj.cate(basis=2) - dml_plr_obj = DoubleMLPLR(dml_data, - ml_l=Lasso(), - ml_m=Lasso(), - n_folds=2) + dml_plr_obj = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=Lasso(), n_folds=2) dml_plr_obj.fit(store_predictions=False) - msg = r'predictions are None. Call .fit\(store_predictions=True\) to store the predictions.' + msg = r"predictions are None. Call .fit\(store_predictions=True\) to store the predictions." with pytest.raises(ValueError, match=msg): dml_plr_obj.cate(basis=2) - dml_data_multiple_treat = DoubleMLData(dml_data.data, y_col="y", d_cols=['d', 'X1']) - dml_plr_obj_multiple = DoubleMLPLR(dml_data_multiple_treat, - ml_l=Lasso(), - ml_m=Lasso(), - n_folds=2) + dml_data_multiple_treat = DoubleMLData(dml_data.data, y_col="y", d_cols=["d", "X1"]) + dml_plr_obj_multiple = DoubleMLPLR(dml_data_multiple_treat, ml_l=Lasso(), ml_m=Lasso(), n_folds=2) dml_plr_obj_multiple.fit() - msg = 'Only implemented for single treatment. Number of treatments is 2.' + msg = "Only implemented for single treatment. Number of treatments is 2." with pytest.raises(NotImplementedError, match=msg): dml_plr_obj_multiple.cate(basis=2) @pytest.mark.ci def test_doubleml_exception_plr_gate(): - dml_plr_obj = DoubleMLPLR(dml_data, - ml_l=Lasso(), - ml_m=Lasso(), - n_folds=2, - n_rep=1) + dml_plr_obj = DoubleMLPLR(dml_data, ml_l=Lasso(), ml_m=Lasso(), n_folds=2, n_rep=1) dml_plr_obj.fit() msg = "Groups must be of DataFrame type. Groups of type was passed." with pytest.raises(TypeError, match=msg): dml_plr_obj.gate(groups=2) - msg = (r'Columns of groups must be of bool type or int type \(dummy coded\). ' - 'Alternatively, groups should only contain one column.') + msg = ( + r"Columns of groups must be of bool type or int type \(dummy coded\). " + "Alternatively, groups should only contain one column." + ) with pytest.raises(TypeError, match=msg): dml_plr_obj.gate(groups=pd.DataFrame(np.random.normal(0, 1, size=(dml_data.n_obs, 3)))) @pytest.mark.ci def test_double_ml_exception_evaluate_learner(): - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATTE') + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATTE" + ) - msg = r'Apply fit\(\) before evaluate_learners\(\).' + msg = r"Apply fit\(\) before evaluate_learners\(\)." with pytest.raises(ValueError, match=msg): dml_irm_obj.evaluate_learners() @@ -1424,26 +1522,22 @@ def test_double_ml_exception_evaluate_learner(): with pytest.raises(TypeError, match=msg): dml_irm_obj.evaluate_learners(metric="mse") - msg = (r"The learners have to be a subset of \['ml_g0', 'ml_g1', 'ml_m'\]. " - r"Learners \['ml_g', 'ml_m'\] provided.") + msg = r"The learners have to be a subset of \['ml_g0', 'ml_g1', 'ml_m'\]. " r"Learners \['ml_g', 'ml_m'\] provided." with pytest.raises(ValueError, match=msg): - dml_irm_obj.evaluate_learners(learners=['ml_g', 'ml_m']) + dml_irm_obj.evaluate_learners(learners=["ml_g", "ml_m"]) - msg = 'Evaluation from learner ml_g0 is not finite.' + msg = "Evaluation from learner ml_g0 is not finite." def eval_fct(y_pred, y_true): return np.nan + with pytest.raises(ValueError, match=msg): dml_irm_obj.evaluate_learners(metric=eval_fct) @pytest.mark.ci def test_doubleml_exception_policytree(): - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5) + dml_irm_obj = DoubleMLIRM(dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5) dml_irm_obj.fit() msg = "Covariates must be of DataFrame type. Covariates of type was passed." @@ -1451,114 +1545,107 @@ def test_doubleml_exception_policytree(): dml_irm_obj.policy_tree(features=2) msg = "Depth must be larger or equal to 0. -1 was passed." with pytest.raises(ValueError, match=msg): - dml_irm_obj.policy_tree(features=pd.DataFrame(np.random.normal(0, 1, size=(dml_data_irm.n_obs, 3))), - depth=-1) + dml_irm_obj.policy_tree(features=pd.DataFrame(np.random.normal(0, 1, size=(dml_data_irm.n_obs, 3))), depth=-1) msg = "Depth must be an integer. 0.1 of type was passed." with pytest.raises(TypeError, match=msg): - dml_irm_obj.policy_tree(features=pd.DataFrame(np.random.normal(0, 1, size=(dml_data_irm.n_obs, 3))), - depth=.1) + dml_irm_obj.policy_tree(features=pd.DataFrame(np.random.normal(0, 1, size=(dml_data_irm.n_obs, 3))), depth=0.1) - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATTE') + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATTE" + ) dml_irm_obj.fit() - msg = 'Invalid score ATTE. Valid score ATE.' + msg = "Invalid score ATTE. Valid score ATE." with pytest.raises(ValueError, match=msg): dml_irm_obj.policy_tree(features=2, depth=1) - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATE', - n_rep=2) + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATE", n_rep=2 + ) dml_irm_obj.fit() - msg = 'Only implemented for one repetition. Number of repetitions is 2.' + msg = "Only implemented for one repetition. Number of repetitions is 2." with pytest.raises(NotImplementedError, match=msg): dml_irm_obj.policy_tree(features=2, depth=1) @pytest.mark.ci def test_double_ml_external_predictions(): - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATE', - n_rep=2) + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATE", n_rep=2 + ) msg = "external_predictions must be a dictionary. ml_m of type was passed." with pytest.raises(TypeError, match=msg): dml_irm_obj.fit(external_predictions="ml_m") - dml_irm_obj = DoubleMLIRM(dml_data_irm, - ml_g=Lasso(), - ml_m=LogisticRegression(), - trimming_threshold=0.05, - n_folds=5, - score='ATE', - n_rep=1) + dml_irm_obj = DoubleMLIRM( + dml_data_irm, ml_g=Lasso(), ml_m=LogisticRegression(), trimming_threshold=0.05, n_folds=5, score="ATE", n_rep=1 + ) - predictions = {'d': 'test', 'd_f': 'test'} - msg = (r"Invalid external_predictions. Invalid treatment variable in \['d', 'd_f'\]. " - "Valid treatment variables d.") + predictions = {"d": "test", "d_f": "test"} + msg = r"Invalid external_predictions. Invalid treatment variable in \['d', 'd_f'\]. " "Valid treatment variables d." with pytest.raises(ValueError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': 'test'} - msg = ("external_predictions must be a nested dictionary. " - "For treatment d a value of type was passed.") + predictions = {"d": "test"} + msg = "external_predictions must be a nested dictionary. " "For treatment d a value of type was passed." with pytest.raises(TypeError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': {'ml_f': 'test'}} - msg = ("Invalid external_predictions. " - r"Invalid nuisance learner for treatment d in \['ml_f'\]. " - "Valid nuisance learners ml_g0 or ml_g1 or ml_m.") + predictions = {"d": {"ml_f": "test"}} + msg = ( + "Invalid external_predictions. " + r"Invalid nuisance learner for treatment d in \['ml_f'\]. " + "Valid nuisance learners ml_g0 or ml_g1 or ml_m." + ) with pytest.raises(ValueError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': {'ml_m': 'test', 'ml_f': 'test'}} - msg = ("Invalid external_predictions. " - r"Invalid nuisance learner for treatment d in \['ml_m', 'ml_f'\]. " - "Valid nuisance learners ml_g0 or ml_g1 or ml_m.") + predictions = {"d": {"ml_m": "test", "ml_f": "test"}} + msg = ( + "Invalid external_predictions. " + r"Invalid nuisance learner for treatment d in \['ml_m', 'ml_f'\]. " + "Valid nuisance learners ml_g0 or ml_g1 or ml_m." + ) with pytest.raises(ValueError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': {'ml_m': 'test'}} - msg = ("Invalid external_predictions. " - "The values of the nested list must be a numpy array. " - "Invalid predictions for treatment d and learner ml_m. " - "Object of type was passed.") + predictions = {"d": {"ml_m": "test"}} + msg = ( + "Invalid external_predictions. " + "The values of the nested list must be a numpy array. " + "Invalid predictions for treatment d and learner ml_m. " + "Object of type was passed." + ) with pytest.raises(TypeError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': {'ml_m': np.array([0])}} - msg = ('Invalid external_predictions. ' - r'The supplied predictions have to be of shape \(100, 1\). ' - 'Invalid predictions for treatment d and learner ml_m. ' - r'Predictions of shape \(1,\) passed.') + predictions = {"d": {"ml_m": np.array([0])}} + msg = ( + "Invalid external_predictions. " + r"The supplied predictions have to be of shape \(100, 1\). " + "Invalid predictions for treatment d and learner ml_m. " + r"Predictions of shape \(1,\) passed." + ) with pytest.raises(ValueError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': {'ml_m': np.zeros(100)}} - msg = ('Invalid external_predictions. ' - r'The supplied predictions have to be of shape \(100, 1\). ' - 'Invalid predictions for treatment d and learner ml_m. ' - r'Predictions of shape \(100,\) passed.') + predictions = {"d": {"ml_m": np.zeros(100)}} + msg = ( + "Invalid external_predictions. " + r"The supplied predictions have to be of shape \(100, 1\). " + "Invalid predictions for treatment d and learner ml_m. " + r"Predictions of shape \(100,\) passed." + ) with pytest.raises(ValueError, match=msg): dml_irm_obj.fit(external_predictions=predictions) - predictions = {'d': {'ml_m': np.ones(shape=(5, 3))}} - msg = ('Invalid external_predictions. ' - r'The supplied predictions have to be of shape \(100, 1\). ' - 'Invalid predictions for treatment d and learner ml_m. ' - r'Predictions of shape \(5, 3\) passed.') + predictions = {"d": {"ml_m": np.ones(shape=(5, 3))}} + msg = ( + "Invalid external_predictions. " + r"The supplied predictions have to be of shape \(100, 1\). " + "Invalid predictions for treatment d and learner ml_m. " + r"Predictions of shape \(5, 3\) passed." + ) with pytest.raises(ValueError, match=msg): dml_irm_obj.fit(external_predictions=predictions) diff --git a/doubleml/tests/test_framework.py b/doubleml/tests/test_framework.py index 5d14feeca..24810b680 100644 --- a/doubleml/tests/test_framework.py +++ b/doubleml/tests/test_framework.py @@ -10,19 +10,17 @@ from ._utils import generate_dml_dict -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 5]) +@pytest.fixture(scope="module", params=[1, 5]) def n_thetas(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_framework_fixture(n_rep, n_thetas): n_obs = 100 @@ -33,13 +31,13 @@ def dml_framework_fixture(n_rep, n_thetas): dml_framework_obj = DoubleMLFramework(doubleml_dict) ci = dml_framework_obj.confint(joint=False, level=0.95) - dml_framework_obj.bootstrap(method='normal') + dml_framework_obj.bootstrap(method="normal") ci_joint = dml_framework_obj.confint(joint=True, level=0.95) # add objects dml_framework_obj_add_obj = dml_framework_obj + dml_framework_obj ci_add_obj = dml_framework_obj_add_obj.confint(joint=False, level=0.95) - dml_framework_obj_add_obj.bootstrap(method='normal') + dml_framework_obj_add_obj.bootstrap(method="normal") ci_joint_add_obj = dml_framework_obj_add_obj.confint(joint=True, level=0.95) # substract objects @@ -49,114 +47,100 @@ def dml_framework_fixture(n_rep, n_thetas): dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) dml_framework_obj_sub_obj = dml_framework_obj - dml_framework_obj_2 ci_sub_obj = dml_framework_obj_sub_obj.confint(joint=False, level=0.95) - dml_framework_obj_sub_obj.bootstrap(method='normal') + dml_framework_obj_sub_obj.bootstrap(method="normal") ci_joint_sub_obj = dml_framework_obj_sub_obj.confint(joint=True, level=0.95) # multiply objects dml_framework_obj_mul_obj = dml_framework_obj * 2 ci_mul_obj = dml_framework_obj_mul_obj.confint(joint=False, level=0.95) - dml_framework_obj_mul_obj.bootstrap(method='normal') + dml_framework_obj_mul_obj.bootstrap(method="normal") ci_joint_mul_obj = dml_framework_obj_mul_obj.confint(joint=True, level=0.95) # concat objects dml_framework_obj_concat = concat([dml_framework_obj, dml_framework_obj]) ci_concat = dml_framework_obj_concat.confint(joint=False, level=0.95) - dml_framework_obj_concat.bootstrap(method='normal') + dml_framework_obj_concat.bootstrap(method="normal") ci_joint_concat = dml_framework_obj_concat.confint(joint=True, level=0.95) result_dict = { - 'dml_dict': doubleml_dict, - 'dml_dict_2': doubleml_dict_2, - 'dml_framework_obj': dml_framework_obj, - 'dml_framework_obj_add_obj': dml_framework_obj_add_obj, - 'dml_framework_obj_sub_obj': dml_framework_obj_sub_obj, - 'dml_framework_obj_mul_obj': dml_framework_obj_mul_obj, - 'dml_framework_obj_concat': dml_framework_obj_concat, - 'ci': ci, - 'ci_add_obj': ci_add_obj, - 'ci_sub_obj': ci_sub_obj, - 'ci_mul_obj': ci_mul_obj, - 'ci_concat': ci_concat, - 'ci_joint': ci_joint, - 'ci_joint_add_obj': ci_joint_add_obj, - 'ci_joint_sub_obj': ci_joint_sub_obj, - 'ci_joint_mul_obj': ci_joint_mul_obj, - 'ci_joint_concat': ci_joint_concat, + "dml_dict": doubleml_dict, + "dml_dict_2": doubleml_dict_2, + "dml_framework_obj": dml_framework_obj, + "dml_framework_obj_add_obj": dml_framework_obj_add_obj, + "dml_framework_obj_sub_obj": dml_framework_obj_sub_obj, + "dml_framework_obj_mul_obj": dml_framework_obj_mul_obj, + "dml_framework_obj_concat": dml_framework_obj_concat, + "ci": ci, + "ci_add_obj": ci_add_obj, + "ci_sub_obj": ci_sub_obj, + "ci_mul_obj": ci_mul_obj, + "ci_concat": ci_concat, + "ci_joint": ci_joint, + "ci_joint_add_obj": ci_joint_add_obj, + "ci_joint_sub_obj": ci_joint_sub_obj, + "ci_joint_mul_obj": ci_joint_mul_obj, + "ci_joint_concat": ci_joint_concat, } return result_dict @pytest.mark.ci def test_dml_framework_theta(dml_framework_fixture): + assert np.allclose(dml_framework_fixture["dml_framework_obj"].all_thetas, dml_framework_fixture["dml_dict"]["all_thetas"]) assert np.allclose( - dml_framework_fixture['dml_framework_obj'].all_thetas, - dml_framework_fixture['dml_dict']['all_thetas'] + dml_framework_fixture["dml_framework_obj_add_obj"].all_thetas, + dml_framework_fixture["dml_dict"]["all_thetas"] + dml_framework_fixture["dml_dict"]["all_thetas"], ) assert np.allclose( - dml_framework_fixture['dml_framework_obj_add_obj'].all_thetas, - dml_framework_fixture['dml_dict']['all_thetas'] + dml_framework_fixture['dml_dict']['all_thetas'] + dml_framework_fixture["dml_framework_obj_sub_obj"].all_thetas, + dml_framework_fixture["dml_dict"]["all_thetas"] - dml_framework_fixture["dml_dict_2"]["all_thetas"], ) assert np.allclose( - dml_framework_fixture['dml_framework_obj_sub_obj'].all_thetas, - dml_framework_fixture['dml_dict']['all_thetas'] - dml_framework_fixture['dml_dict_2']['all_thetas'] + dml_framework_fixture["dml_framework_obj_mul_obj"].all_thetas, 2 * dml_framework_fixture["dml_dict"]["all_thetas"] ) assert np.allclose( - dml_framework_fixture['dml_framework_obj_mul_obj'].all_thetas, - 2*dml_framework_fixture['dml_dict']['all_thetas'] - ) - assert np.allclose( - dml_framework_fixture['dml_framework_obj_concat'].all_thetas, - np.vstack((dml_framework_fixture['dml_dict']['all_thetas'], dml_framework_fixture['dml_dict']['all_thetas'])) + dml_framework_fixture["dml_framework_obj_concat"].all_thetas, + np.vstack((dml_framework_fixture["dml_dict"]["all_thetas"], dml_framework_fixture["dml_dict"]["all_thetas"])), ) @pytest.mark.ci def test_dml_framework_se(dml_framework_fixture): - assert np.allclose( - dml_framework_fixture['dml_framework_obj'].all_ses, - dml_framework_fixture['dml_dict']['all_ses'] - ) - scaling = dml_framework_fixture['dml_dict']['var_scaling_factors'].reshape(-1, 1) + assert np.allclose(dml_framework_fixture["dml_framework_obj"].all_ses, dml_framework_fixture["dml_dict"]["all_ses"]) + scaling = dml_framework_fixture["dml_dict"]["var_scaling_factors"].reshape(-1, 1) add_var = np.mean( - np.square(dml_framework_fixture['dml_dict']['scaled_psi'] + dml_framework_fixture['dml_dict']['scaled_psi']), - axis=0) - assert np.allclose( - dml_framework_fixture['dml_framework_obj_add_obj'].all_ses, - np.sqrt(add_var / scaling) + np.square(dml_framework_fixture["dml_dict"]["scaled_psi"] + dml_framework_fixture["dml_dict"]["scaled_psi"]), axis=0 ) - scaling = dml_framework_fixture['dml_dict']['var_scaling_factors'].reshape(-1, 1) + assert np.allclose(dml_framework_fixture["dml_framework_obj_add_obj"].all_ses, np.sqrt(add_var / scaling)) + scaling = dml_framework_fixture["dml_dict"]["var_scaling_factors"].reshape(-1, 1) sub_var = np.mean( - np.square(dml_framework_fixture['dml_dict']['scaled_psi'] - dml_framework_fixture['dml_dict_2']['scaled_psi']), - axis=0) - assert np.allclose( - dml_framework_fixture['dml_framework_obj_sub_obj'].all_ses, - np.sqrt(sub_var / scaling) + np.square(dml_framework_fixture["dml_dict"]["scaled_psi"] - dml_framework_fixture["dml_dict_2"]["scaled_psi"]), axis=0 ) + assert np.allclose(dml_framework_fixture["dml_framework_obj_sub_obj"].all_ses, np.sqrt(sub_var / scaling)) assert np.allclose( - dml_framework_fixture['dml_framework_obj_mul_obj'].all_ses, - 2*dml_framework_fixture['dml_dict']['all_ses'] + dml_framework_fixture["dml_framework_obj_mul_obj"].all_ses, 2 * dml_framework_fixture["dml_dict"]["all_ses"] ) assert np.allclose( - dml_framework_fixture['dml_framework_obj_concat'].all_ses, - np.vstack((dml_framework_fixture['dml_dict']['all_ses'], dml_framework_fixture['dml_dict']['all_ses'])) + dml_framework_fixture["dml_framework_obj_concat"].all_ses, + np.vstack((dml_framework_fixture["dml_dict"]["all_ses"], dml_framework_fixture["dml_dict"]["all_ses"])), ) @pytest.mark.ci def test_dml_framework_ci(dml_framework_fixture): - assert isinstance(dml_framework_fixture['ci'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_joint'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_add_obj'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_joint_add_obj'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_sub_obj'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_joint_sub_obj'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_mul_obj'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_joint_mul_obj'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_concat'], pd.DataFrame) - assert isinstance(dml_framework_fixture['ci_joint_concat'], pd.DataFrame) - - -@pytest.fixture(scope='module') + assert isinstance(dml_framework_fixture["ci"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_joint"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_add_obj"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_joint_add_obj"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_sub_obj"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_joint_sub_obj"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_mul_obj"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_joint_mul_obj"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_concat"], pd.DataFrame) + assert isinstance(dml_framework_fixture["ci_joint_concat"], pd.DataFrame) + + +@pytest.fixture(scope="module") def dml_framework_from_doubleml_fixture(n_rep): dml_data = make_irm_data() @@ -168,13 +152,13 @@ def dml_framework_from_doubleml_fixture(n_rep): dml_framework_obj = dml_irm_obj.construct_framework() ci = dml_framework_obj.confint(joint=False, level=0.95) - dml_framework_obj.bootstrap(method='normal') + dml_framework_obj.bootstrap(method="normal") ci_joint = dml_framework_obj.confint(joint=True, level=0.95) # add objects dml_framework_obj_add_obj = dml_framework_obj + dml_framework_obj ci_add_obj = dml_framework_obj_add_obj.confint(joint=False, level=0.95) - dml_framework_obj_add_obj.bootstrap(method='normal') + dml_framework_obj_add_obj.bootstrap(method="normal") ci_joint_add_obj = dml_framework_obj_add_obj.confint(joint=True, level=0.95) # substract objects @@ -185,40 +169,40 @@ def dml_framework_from_doubleml_fixture(n_rep): dml_framework_obj_sub_obj = dml_framework_obj - dml_framework_obj_2 ci_sub_obj = dml_framework_obj_sub_obj.confint(joint=False, level=0.95) - dml_framework_obj_sub_obj.bootstrap(method='normal') + dml_framework_obj_sub_obj.bootstrap(method="normal") ci_joint_sub_obj = dml_framework_obj_sub_obj.confint(joint=True, level=0.95) # multiply objects dml_framework_obj_mul_obj = dml_framework_obj * 2 ci_mul_obj = dml_framework_obj_mul_obj.confint(joint=False, level=0.95) - dml_framework_obj_mul_obj.bootstrap(method='normal') + dml_framework_obj_mul_obj.bootstrap(method="normal") ci_joint_mul_obj = dml_framework_obj_mul_obj.confint(joint=True, level=0.95) # concat objects dml_framework_obj_concat = concat([dml_framework_obj, dml_framework_obj]) ci_concat = dml_framework_obj_concat.confint(joint=False, level=0.95) - dml_framework_obj_concat.bootstrap(method='normal') + dml_framework_obj_concat.bootstrap(method="normal") ci_joint_concat = dml_framework_obj_concat.confint(joint=True, level=0.95) result_dict = { - 'dml_obj': dml_irm_obj, - 'dml_obj_2': dml_irm_obj_2, - 'dml_framework_obj': dml_framework_obj, - 'dml_framework_obj_add_obj': dml_framework_obj_add_obj, - 'dml_framework_obj_sub_obj': dml_framework_obj_sub_obj, - 'dml_framework_obj_mul_obj': dml_framework_obj_mul_obj, - 'dml_framework_obj_concat': dml_framework_obj_concat, - 'ci': ci, - 'ci_add_obj': ci_add_obj, - 'ci_sub_obj': ci_sub_obj, - 'ci_mul_obj': ci_mul_obj, - 'ci_concat': ci_concat, - 'ci_joint': ci_joint, - 'ci_joint_add_obj': ci_joint_add_obj, - 'ci_joint_sub_obj': ci_joint_sub_obj, - 'ci_joint_mul_obj': ci_joint_mul_obj, - 'ci_joint_concat': ci_joint_concat, - 'n_rep': n_rep, + "dml_obj": dml_irm_obj, + "dml_obj_2": dml_irm_obj_2, + "dml_framework_obj": dml_framework_obj, + "dml_framework_obj_add_obj": dml_framework_obj_add_obj, + "dml_framework_obj_sub_obj": dml_framework_obj_sub_obj, + "dml_framework_obj_mul_obj": dml_framework_obj_mul_obj, + "dml_framework_obj_concat": dml_framework_obj_concat, + "ci": ci, + "ci_add_obj": ci_add_obj, + "ci_sub_obj": ci_sub_obj, + "ci_mul_obj": ci_mul_obj, + "ci_concat": ci_concat, + "ci_joint": ci_joint, + "ci_joint_add_obj": ci_joint_add_obj, + "ci_joint_sub_obj": ci_joint_sub_obj, + "ci_joint_mul_obj": ci_joint_mul_obj, + "ci_joint_concat": ci_joint_concat, + "n_rep": n_rep, } return result_dict @@ -226,71 +210,73 @@ def dml_framework_from_doubleml_fixture(n_rep): @pytest.mark.ci def test_dml_framework_from_doubleml_theta(dml_framework_from_doubleml_fixture): assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj'].all_thetas, - dml_framework_from_doubleml_fixture['dml_obj'].all_coef + dml_framework_from_doubleml_fixture["dml_framework_obj"].all_thetas, + dml_framework_from_doubleml_fixture["dml_obj"].all_coef, ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_add_obj'].all_thetas, - dml_framework_from_doubleml_fixture['dml_obj'].all_coef + dml_framework_from_doubleml_fixture['dml_obj'].all_coef + dml_framework_from_doubleml_fixture["dml_framework_obj_add_obj"].all_thetas, + dml_framework_from_doubleml_fixture["dml_obj"].all_coef + dml_framework_from_doubleml_fixture["dml_obj"].all_coef, ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_sub_obj'].all_thetas, - dml_framework_from_doubleml_fixture['dml_obj'].all_coef - dml_framework_from_doubleml_fixture['dml_obj_2'].all_coef + dml_framework_from_doubleml_fixture["dml_framework_obj_sub_obj"].all_thetas, + dml_framework_from_doubleml_fixture["dml_obj"].all_coef - dml_framework_from_doubleml_fixture["dml_obj_2"].all_coef, ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_mul_obj'].all_thetas, - 2*dml_framework_from_doubleml_fixture['dml_obj'].all_coef + dml_framework_from_doubleml_fixture["dml_framework_obj_mul_obj"].all_thetas, + 2 * dml_framework_from_doubleml_fixture["dml_obj"].all_coef, ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_concat'].all_thetas, - np.vstack((dml_framework_from_doubleml_fixture['dml_obj'].all_coef, - dml_framework_from_doubleml_fixture['dml_obj'].all_coef)) + dml_framework_from_doubleml_fixture["dml_framework_obj_concat"].all_thetas, + np.vstack( + (dml_framework_from_doubleml_fixture["dml_obj"].all_coef, dml_framework_from_doubleml_fixture["dml_obj"].all_coef) + ), ) @pytest.mark.ci def test_dml_framework_from_doubleml_se(dml_framework_from_doubleml_fixture): assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj'].all_ses, - dml_framework_from_doubleml_fixture['dml_obj'].all_se + dml_framework_from_doubleml_fixture["dml_framework_obj"].all_ses, dml_framework_from_doubleml_fixture["dml_obj"].all_se ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_add_obj'].all_ses, - 2*dml_framework_from_doubleml_fixture['dml_obj'].all_se + dml_framework_from_doubleml_fixture["dml_framework_obj_add_obj"].all_ses, + 2 * dml_framework_from_doubleml_fixture["dml_obj"].all_se, ) - if dml_framework_from_doubleml_fixture['n_rep'] == 1: + if dml_framework_from_doubleml_fixture["n_rep"] == 1: # formula only valid for n_rep = 1 - scaling = np.array([dml_framework_from_doubleml_fixture['dml_obj']._var_scaling_factors]).reshape(-1, 1) + scaling = np.array([dml_framework_from_doubleml_fixture["dml_obj"]._var_scaling_factors]).reshape(-1, 1) sub_var = np.mean( - np.square(dml_framework_from_doubleml_fixture['dml_obj'].psi - - dml_framework_from_doubleml_fixture['dml_obj_2'].psi), - axis=0) + np.square( + dml_framework_from_doubleml_fixture["dml_obj"].psi - dml_framework_from_doubleml_fixture["dml_obj_2"].psi + ), + axis=0, + ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_sub_obj'].all_ses, - np.sqrt(sub_var / scaling) + dml_framework_from_doubleml_fixture["dml_framework_obj_sub_obj"].all_ses, np.sqrt(sub_var / scaling) ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_mul_obj'].all_ses, - 2*dml_framework_from_doubleml_fixture['dml_obj'].all_se + dml_framework_from_doubleml_fixture["dml_framework_obj_mul_obj"].all_ses, + 2 * dml_framework_from_doubleml_fixture["dml_obj"].all_se, ) assert np.allclose( - dml_framework_from_doubleml_fixture['dml_framework_obj_concat'].all_ses, - np.vstack((dml_framework_from_doubleml_fixture['dml_obj'].all_se, - dml_framework_from_doubleml_fixture['dml_obj'].all_se)) + dml_framework_from_doubleml_fixture["dml_framework_obj_concat"].all_ses, + np.vstack( + (dml_framework_from_doubleml_fixture["dml_obj"].all_se, dml_framework_from_doubleml_fixture["dml_obj"].all_se) + ), ) @pytest.mark.ci def test_dml_framework_from_doubleml_ci(dml_framework_from_doubleml_fixture): - assert isinstance(dml_framework_from_doubleml_fixture['ci'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_joint'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_add_obj'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_joint_add_obj'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_sub_obj'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_joint_sub_obj'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_mul_obj'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_joint_mul_obj'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_concat'], pd.DataFrame) - assert isinstance(dml_framework_from_doubleml_fixture['ci_joint_concat'], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_joint"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_add_obj"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_joint_add_obj"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_sub_obj"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_joint_sub_obj"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_mul_obj"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_joint_mul_obj"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_concat"], pd.DataFrame) + assert isinstance(dml_framework_from_doubleml_fixture["ci_joint_concat"], pd.DataFrame) diff --git a/doubleml/tests/test_framework_coverage.py b/doubleml/tests/test_framework_coverage.py index 11d658a55..03625cef2 100644 --- a/doubleml/tests/test_framework_coverage.py +++ b/doubleml/tests/test_framework_coverage.py @@ -6,19 +6,17 @@ from ._utils import generate_dml_dict -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 5]) +@pytest.fixture(scope="module", params=[1, 5]) def n_thetas(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def test_dml_framework_coverage_fixture(n_rep, n_thetas): np.random.seed(42) R = 1000 @@ -42,8 +40,8 @@ def test_dml_framework_coverage_fixture(n_rep, n_thetas): coverage_joint_mul_obj = np.zeros((R, 1)) coverage_all_cis_joint_mul_obj = np.zeros((R, 1, n_rep)) - coverage_concat = np.zeros((R, 2*n_thetas)) - coverage_all_cis_concat = np.zeros((R, 2*n_thetas, n_rep)) + coverage_concat = np.zeros((R, 2 * n_thetas)) + coverage_all_cis_concat = np.zeros((R, 2 * n_thetas, n_rep)) coverage_joint_concat = np.zeros((R, 1)) coverage_all_cis_joint_concat = np.zeros((R, 1, n_rep)) for r in range(R): @@ -63,163 +61,193 @@ def test_dml_framework_coverage_fixture(n_rep, n_thetas): true_thetas = np.vstack((np.repeat(0.0, n_thetas), np.repeat(-1.0, n_thetas))).transpose() ci = dml_framework_obj_1.confint(joint=False, level=0.95) - coverage[r, :] = (true_thetas[:, 0] >= ci['2.5 %'].to_numpy()) & (true_thetas[:, 0] <= ci['97.5 %'].to_numpy()) - coverage_all_cis[r, :, :] = (true_thetas[:, 0].reshape(-1, 1) >= dml_framework_obj_1._all_cis[:, 0, :]) & \ - (true_thetas[:, 0].reshape(-1, 1) <= dml_framework_obj_1._all_cis[:, 1, :]) + coverage[r, :] = (true_thetas[:, 0] >= ci["2.5 %"].to_numpy()) & (true_thetas[:, 0] <= ci["97.5 %"].to_numpy()) + coverage_all_cis[r, :, :] = (true_thetas[:, 0].reshape(-1, 1) >= dml_framework_obj_1._all_cis[:, 0, :]) & ( + true_thetas[:, 0].reshape(-1, 1) <= dml_framework_obj_1._all_cis[:, 1, :] + ) # joint confidence interval - dml_framework_obj_1.bootstrap(method='normal') + dml_framework_obj_1.bootstrap(method="normal") ci_joint = dml_framework_obj_1.confint(joint=True, level=0.95) coverage_joint[r, :] = np.all( - (true_thetas[:, 0] >= ci_joint['2.5 %'].to_numpy()) & - (true_thetas[:, 0] <= ci_joint['97.5 %'].to_numpy())) + (true_thetas[:, 0] >= ci_joint["2.5 %"].to_numpy()) & (true_thetas[:, 0] <= ci_joint["97.5 %"].to_numpy()) + ) coverage_all_cis_joint[r, :, :] = np.all( - (true_thetas[:, 0].reshape(-1, 1) >= dml_framework_obj_1._all_cis[:, 0, :]) & - (true_thetas[:, 0].reshape(-1, 1) <= dml_framework_obj_1._all_cis[:, 1, :]), - axis=0) + (true_thetas[:, 0].reshape(-1, 1) >= dml_framework_obj_1._all_cis[:, 0, :]) + & (true_thetas[:, 0].reshape(-1, 1) <= dml_framework_obj_1._all_cis[:, 1, :]), + axis=0, + ) # add objects dml_framework_obj_add_obj = dml_framework_obj_1 + dml_framework_obj_2 true_thetas_add_obj = np.sum(true_thetas, axis=1) ci_add_obj = dml_framework_obj_add_obj.confint(joint=False, level=0.95) - coverage_add_obj[r, :] = ( - (true_thetas_add_obj >= ci_add_obj['2.5 %'].to_numpy()) & - (true_thetas_add_obj <= ci_add_obj['97.5 %'].to_numpy())) + coverage_add_obj[r, :] = (true_thetas_add_obj >= ci_add_obj["2.5 %"].to_numpy()) & ( + true_thetas_add_obj <= ci_add_obj["97.5 %"].to_numpy() + ) coverage_all_cis_add_obj[r, :, :] = ( - (true_thetas_add_obj.reshape(-1, 1) >= dml_framework_obj_add_obj._all_cis[:, 0, :]) & - (true_thetas_add_obj.reshape(-1, 1) <= dml_framework_obj_add_obj._all_cis[:, 1, :])) + true_thetas_add_obj.reshape(-1, 1) >= dml_framework_obj_add_obj._all_cis[:, 0, :] + ) & (true_thetas_add_obj.reshape(-1, 1) <= dml_framework_obj_add_obj._all_cis[:, 1, :]) - dml_framework_obj_add_obj.bootstrap(method='normal') + dml_framework_obj_add_obj.bootstrap(method="normal") ci_joint_add_obj = dml_framework_obj_add_obj.confint(joint=True, level=0.95) coverage_joint_add_obj[r, :] = np.all( - (true_thetas_add_obj >= ci_joint_add_obj['2.5 %'].to_numpy()) & - (true_thetas_add_obj <= ci_joint_add_obj['97.5 %'].to_numpy())) + (true_thetas_add_obj >= ci_joint_add_obj["2.5 %"].to_numpy()) + & (true_thetas_add_obj <= ci_joint_add_obj["97.5 %"].to_numpy()) + ) coverage_all_cis_joint_add_obj[r, :, :] = np.all( - (true_thetas_add_obj.reshape(-1, 1) >= dml_framework_obj_add_obj._all_cis[:, 0, :]) & - (true_thetas_add_obj.reshape(-1, 1) <= dml_framework_obj_add_obj._all_cis[:, 1, :]), - axis=0) + (true_thetas_add_obj.reshape(-1, 1) >= dml_framework_obj_add_obj._all_cis[:, 0, :]) + & (true_thetas_add_obj.reshape(-1, 1) <= dml_framework_obj_add_obj._all_cis[:, 1, :]), + axis=0, + ) # substract objects dml_framework_obj_sub_obj = dml_framework_obj_1 - dml_framework_obj_2 true_thetas_sub_obj = true_thetas[:, 0] - true_thetas[:, 1] ci_sub_obj = dml_framework_obj_sub_obj.confint(joint=False, level=0.95) - coverage_sub_obj[r, :] = ( - (true_thetas_sub_obj >= ci_sub_obj['2.5 %'].to_numpy()) & - (true_thetas_sub_obj <= ci_sub_obj['97.5 %'].to_numpy())) + coverage_sub_obj[r, :] = (true_thetas_sub_obj >= ci_sub_obj["2.5 %"].to_numpy()) & ( + true_thetas_sub_obj <= ci_sub_obj["97.5 %"].to_numpy() + ) coverage_all_cis_sub_obj[r, :, :] = ( - (true_thetas_sub_obj.reshape(-1, 1) >= dml_framework_obj_sub_obj._all_cis[:, 0, :]) & - (true_thetas_sub_obj.reshape(-1, 1) <= dml_framework_obj_sub_obj._all_cis[:, 1, :])) + true_thetas_sub_obj.reshape(-1, 1) >= dml_framework_obj_sub_obj._all_cis[:, 0, :] + ) & (true_thetas_sub_obj.reshape(-1, 1) <= dml_framework_obj_sub_obj._all_cis[:, 1, :]) - dml_framework_obj_sub_obj.bootstrap(method='normal') + dml_framework_obj_sub_obj.bootstrap(method="normal") ci_joint_sub_obj = dml_framework_obj_sub_obj.confint(joint=True, level=0.95) coverage_joint_sub_obj[r, :] = np.all( - (true_thetas_sub_obj >= ci_joint_sub_obj['2.5 %'].to_numpy()) & - (true_thetas_sub_obj <= ci_joint_sub_obj['97.5 %'].to_numpy())) + (true_thetas_sub_obj >= ci_joint_sub_obj["2.5 %"].to_numpy()) + & (true_thetas_sub_obj <= ci_joint_sub_obj["97.5 %"].to_numpy()) + ) coverage_all_cis_joint_sub_obj[r, :, :] = np.all( - (true_thetas_sub_obj.reshape(-1, 1) >= dml_framework_obj_sub_obj._all_cis[:, 0, :]) & - (true_thetas_sub_obj.reshape(-1, 1) <= dml_framework_obj_sub_obj._all_cis[:, 1, :]), - axis=0) + (true_thetas_sub_obj.reshape(-1, 1) >= dml_framework_obj_sub_obj._all_cis[:, 0, :]) + & (true_thetas_sub_obj.reshape(-1, 1) <= dml_framework_obj_sub_obj._all_cis[:, 1, :]), + axis=0, + ) # multiply objects dml_framework_obj_mul_obj = dml_framework_obj_2 * 2 true_thetas_mul_obj = 2 * true_thetas[:, 1] ci_mul_obj = dml_framework_obj_mul_obj.confint(joint=False, level=0.95) - coverage_mul_obj[r, :] = ( - (true_thetas_mul_obj >= ci_mul_obj['2.5 %'].to_numpy()) & - (true_thetas_mul_obj <= ci_mul_obj['97.5 %'].to_numpy())) + coverage_mul_obj[r, :] = (true_thetas_mul_obj >= ci_mul_obj["2.5 %"].to_numpy()) & ( + true_thetas_mul_obj <= ci_mul_obj["97.5 %"].to_numpy() + ) coverage_all_cis_mul_obj[r, :, :] = ( - (true_thetas_mul_obj.reshape(-1, 1) >= dml_framework_obj_mul_obj._all_cis[:, 0, :]) & - (true_thetas_mul_obj.reshape(-1, 1) <= dml_framework_obj_mul_obj._all_cis[:, 1, :])) + true_thetas_mul_obj.reshape(-1, 1) >= dml_framework_obj_mul_obj._all_cis[:, 0, :] + ) & (true_thetas_mul_obj.reshape(-1, 1) <= dml_framework_obj_mul_obj._all_cis[:, 1, :]) - dml_framework_obj_mul_obj.bootstrap(method='normal') + dml_framework_obj_mul_obj.bootstrap(method="normal") ci_joint_mul_obj = dml_framework_obj_mul_obj.confint(joint=True, level=0.95) coverage_joint_mul_obj[r, :] = np.all( - (true_thetas_mul_obj >= ci_joint_mul_obj['2.5 %'].to_numpy()) & - (true_thetas_mul_obj <= ci_joint_mul_obj['97.5 %'].to_numpy())) + (true_thetas_mul_obj >= ci_joint_mul_obj["2.5 %"].to_numpy()) + & (true_thetas_mul_obj <= ci_joint_mul_obj["97.5 %"].to_numpy()) + ) coverage_all_cis_joint_mul_obj[r, :, :] = np.all( - (true_thetas_mul_obj.reshape(-1, 1) >= dml_framework_obj_mul_obj._all_cis[:, 0, :]) & - (true_thetas_mul_obj.reshape(-1, 1) <= dml_framework_obj_mul_obj._all_cis[:, 1, :]), - axis=0) + (true_thetas_mul_obj.reshape(-1, 1) >= dml_framework_obj_mul_obj._all_cis[:, 0, :]) + & (true_thetas_mul_obj.reshape(-1, 1) <= dml_framework_obj_mul_obj._all_cis[:, 1, :]), + axis=0, + ) # concat objects dml_framework_obj_concat = concat([dml_framework_obj_1, dml_framework_obj_2]) - true_thetas_concat = true_thetas.reshape(-1, order='F') + true_thetas_concat = true_thetas.reshape(-1, order="F") ci_concat = dml_framework_obj_concat.confint(joint=False, level=0.95) - coverage_concat[r, :] = ( - (true_thetas_concat >= ci_concat['2.5 %'].to_numpy()) & - (true_thetas_concat <= ci_concat['97.5 %'].to_numpy())) + coverage_concat[r, :] = (true_thetas_concat >= ci_concat["2.5 %"].to_numpy()) & ( + true_thetas_concat <= ci_concat["97.5 %"].to_numpy() + ) coverage_all_cis_concat[r, :, :] = ( - (true_thetas_concat.reshape(-1, 1) >= dml_framework_obj_concat._all_cis[:, 0, :]) & - (true_thetas_concat.reshape(-1, 1) <= dml_framework_obj_concat._all_cis[:, 1, :])) + true_thetas_concat.reshape(-1, 1) >= dml_framework_obj_concat._all_cis[:, 0, :] + ) & (true_thetas_concat.reshape(-1, 1) <= dml_framework_obj_concat._all_cis[:, 1, :]) - dml_framework_obj_concat.bootstrap(method='normal') + dml_framework_obj_concat.bootstrap(method="normal") ci_joint_concat = dml_framework_obj_concat.confint(joint=True, level=0.95) coverage_joint_concat[r, :] = np.all( - (true_thetas_concat >= ci_joint_concat['2.5 %'].to_numpy()) & - (true_thetas_concat <= ci_joint_concat['97.5 %'].to_numpy())) + (true_thetas_concat >= ci_joint_concat["2.5 %"].to_numpy()) + & (true_thetas_concat <= ci_joint_concat["97.5 %"].to_numpy()) + ) coverage_all_cis_joint_concat[r, :, :] = np.all( - (true_thetas_concat.reshape(-1, 1) >= dml_framework_obj_concat._all_cis[:, 0, :]) & - (true_thetas_concat.reshape(-1, 1) <= dml_framework_obj_concat._all_cis[:, 1, :]), - axis=0) + (true_thetas_concat.reshape(-1, 1) >= dml_framework_obj_concat._all_cis[:, 0, :]) + & (true_thetas_concat.reshape(-1, 1) <= dml_framework_obj_concat._all_cis[:, 1, :]), + axis=0, + ) result_dict = { - 'coverage_rate': np.mean(coverage, axis=0), - 'coverage_rate_all_cis': np.mean(coverage_all_cis, axis=0), - 'coverage_rate_joint': np.mean(coverage_joint, axis=0), - 'coverage_rate_all_cis_joint': np.mean(coverage_all_cis_joint, axis=0), - 'coverage_rate_add_obj': np.mean(coverage_add_obj, axis=0), - 'coverage_rate_all_cis_add_obj': np.mean(coverage_all_cis_add_obj, axis=0), - 'coverage_rate_joint_add_obj': np.mean(coverage_joint_add_obj, axis=0), - 'coverage_rate_all_cis_joint_add_obj': np.mean(coverage_all_cis_joint_add_obj, axis=0), - 'coverage_rate_sub_obj': np.mean(coverage_sub_obj, axis=0), - 'coverage_rate_all_cis_sub_obj': np.mean(coverage_all_cis_sub_obj, axis=0), - 'coverage_rate_joint_sub_obj': np.mean(coverage_joint_sub_obj, axis=0), - 'coverage_rate_all_cis_joint_sub_obj': np.mean(coverage_all_cis_joint_sub_obj, axis=0), - 'coverage_rate_mul_obj': np.mean(coverage_mul_obj, axis=0), - 'coverage_rate_all_cis_mul_obj': np.mean(coverage_all_cis_mul_obj, axis=0), - 'coverage_rate_joint_mul_obj': np.mean(coverage_joint_mul_obj, axis=0), - 'coverage_rate_all_cis_joint_mul_obj': np.mean(coverage_all_cis_joint_mul_obj, axis=0), - 'coverage_rate_concat': np.mean(coverage_concat, axis=0), - 'coverage_rate_all_cis_concat': np.mean(coverage_all_cis_concat, axis=0), - 'coverage_rate_joint_concat': np.mean(coverage_joint_concat, axis=0), - 'coverage_rate_all_cis_joint_concat': np.mean(coverage_all_cis_joint_concat, axis=0), + "coverage_rate": np.mean(coverage, axis=0), + "coverage_rate_all_cis": np.mean(coverage_all_cis, axis=0), + "coverage_rate_joint": np.mean(coverage_joint, axis=0), + "coverage_rate_all_cis_joint": np.mean(coverage_all_cis_joint, axis=0), + "coverage_rate_add_obj": np.mean(coverage_add_obj, axis=0), + "coverage_rate_all_cis_add_obj": np.mean(coverage_all_cis_add_obj, axis=0), + "coverage_rate_joint_add_obj": np.mean(coverage_joint_add_obj, axis=0), + "coverage_rate_all_cis_joint_add_obj": np.mean(coverage_all_cis_joint_add_obj, axis=0), + "coverage_rate_sub_obj": np.mean(coverage_sub_obj, axis=0), + "coverage_rate_all_cis_sub_obj": np.mean(coverage_all_cis_sub_obj, axis=0), + "coverage_rate_joint_sub_obj": np.mean(coverage_joint_sub_obj, axis=0), + "coverage_rate_all_cis_joint_sub_obj": np.mean(coverage_all_cis_joint_sub_obj, axis=0), + "coverage_rate_mul_obj": np.mean(coverage_mul_obj, axis=0), + "coverage_rate_all_cis_mul_obj": np.mean(coverage_all_cis_mul_obj, axis=0), + "coverage_rate_joint_mul_obj": np.mean(coverage_joint_mul_obj, axis=0), + "coverage_rate_all_cis_joint_mul_obj": np.mean(coverage_all_cis_joint_mul_obj, axis=0), + "coverage_rate_concat": np.mean(coverage_concat, axis=0), + "coverage_rate_all_cis_concat": np.mean(coverage_all_cis_concat, axis=0), + "coverage_rate_joint_concat": np.mean(coverage_joint_concat, axis=0), + "coverage_rate_all_cis_joint_concat": np.mean(coverage_all_cis_joint_concat, axis=0), } return result_dict @pytest.mark.ci def test_dml_framework_coverage(test_dml_framework_coverage_fixture): - assert np.all((test_dml_framework_coverage_fixture['coverage_rate'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_add_obj'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_add_obj'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_add_obj'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_sub_obj'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_sub_obj'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_sub_obj'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_mul_obj'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_mul_obj'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_mul_obj'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_concat'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_concat'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_concat'] < 1.0)) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_add_obj"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_add_obj"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_add_obj"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_sub_obj"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_sub_obj"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_sub_obj"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_mul_obj"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_mul_obj"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_mul_obj"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_concat"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_concat"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_concat"] < 1.0) + ) @pytest.mark.ci def test_dml_framework_coverage_joint(test_dml_framework_coverage_fixture): - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_joint'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_joint_add_obj'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_add_obj'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_add_obj'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_joint_sub_obj'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_sub_obj'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_sub_obj'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_joint_mul_obj'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_mul_obj'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_mul_obj'] < 1.0)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_joint_concat'] >= 0.9)) - assert np.all((test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_concat'] >= 0.9) & - (test_dml_framework_coverage_fixture['coverage_rate_all_cis_joint_concat'] < 1.0)) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_joint"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_joint_add_obj"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_add_obj"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_add_obj"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_joint_sub_obj"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_sub_obj"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_sub_obj"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_joint_mul_obj"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_mul_obj"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_mul_obj"] < 1.0) + ) + assert np.all((test_dml_framework_coverage_fixture["coverage_rate_joint_concat"] >= 0.9)) + assert np.all( + (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_concat"] >= 0.9) + & (test_dml_framework_coverage_fixture["coverage_rate_all_cis_joint_concat"] < 1.0) + ) diff --git a/doubleml/tests/test_framework_exceptions.py b/doubleml/tests/test_framework_exceptions.py index 12e7b2d33..b7ee2b8de 100644 --- a/doubleml/tests/test_framework_exceptions.py +++ b/doubleml/tests/test_framework_exceptions.py @@ -16,12 +16,12 @@ psi_b = np.random.normal(size=(n_obs, n_thetas, n_rep)) doubleml_dict = generate_dml_dict(psi_a, psi_b) # add sensitivity elements -doubleml_dict['sensitivity_elements'] = { - 'sigma2': np.ones(shape=(1, n_thetas, n_rep)), - 'nu2': np.ones(shape=(1, n_thetas, n_rep)), - 'psi_sigma2': np.ones(shape=(n_obs, n_thetas, n_rep)), - 'psi_nu2': np.ones(shape=(n_obs, n_thetas, n_rep)), - 'riesz_rep': np.ones(shape=(n_obs, n_thetas, n_rep)) +doubleml_dict["sensitivity_elements"] = { + "sigma2": np.ones(shape=(1, n_thetas, n_rep)), + "nu2": np.ones(shape=(1, n_thetas, n_rep)), + "psi_sigma2": np.ones(shape=(n_obs, n_thetas, n_rep)), + "psi_nu2": np.ones(shape=(n_obs, n_thetas, n_rep)), + "riesz_rep": np.ones(shape=(n_obs, n_thetas, n_rep)), } # combine objects and estimate parameters @@ -38,37 +38,37 @@ def test_input_exceptions(): msg = r"The shape of thetas does not match the expected shape \(2,\)\." with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['thetas'] = np.ones(shape=(1,)) + test_dict["thetas"] = np.ones(shape=(1,)) DoubleMLFramework(test_dict) msg = r"The shape of ses does not match the expected shape \(2,\)\." with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['ses'] = np.ones(shape=(1,)) + test_dict["ses"] = np.ones(shape=(1,)) DoubleMLFramework(test_dict) msg = r"The shape of all_thetas does not match the expected shape \(2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['all_thetas'] = np.ones(shape=(1, 5)) + test_dict["all_thetas"] = np.ones(shape=(1, 5)) DoubleMLFramework(test_dict) msg = r"The shape of all_ses does not match the expected shape \(2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['all_ses'] = np.ones(shape=(1, 5)) + test_dict["all_ses"] = np.ones(shape=(1, 5)) DoubleMLFramework(test_dict) msg = r"The shape of var_scaling_factors does not match the expected shape \(2,\)\." with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['var_scaling_factors'] = np.ones(shape=(1, 5)) + test_dict["var_scaling_factors"] = np.ones(shape=(1, 5)) DoubleMLFramework(test_dict) msg = r"The shape of scaled_psi does not match the expected shape \(10, 2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['scaled_psi'] = np.ones(shape=(10, 2, 5, 3)) + test_dict["scaled_psi"] = np.ones(shape=(10, 2, 5, 3)) DoubleMLFramework(test_dict) msg = "doubleml_dict must be a dictionary." @@ -78,70 +78,72 @@ def test_input_exceptions(): msg = "sensitivity_elements must be a dictionary." with pytest.raises(TypeError, match=msg): test_dict = doubleml_dict.copy() - test_dict['sensitivity_elements'] = 1 + test_dict["sensitivity_elements"] = 1 DoubleMLFramework(test_dict) - msg = 'The sensitivity_elements dict must contain the following keys: sigma2, nu2, psi_sigma2, psi_nu2' + msg = "The sensitivity_elements dict must contain the following keys: sigma2, nu2, psi_sigma2, psi_nu2" with pytest.raises(ValueError, match=msg): test_dict = doubleml_dict.copy() - test_dict['sensitivity_elements'] = {'sensitivities': np.ones(shape=(n_obs, n_thetas, n_rep))} + test_dict["sensitivity_elements"] = {"sensitivities": np.ones(shape=(n_obs, n_thetas, n_rep))} DoubleMLFramework(test_dict) - msg = r'The shape of sigma2 does not match the expected shape \(1, 2, 5\)\.' + msg = r"The shape of sigma2 does not match the expected shape \(1, 2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['sensitivity_elements']['sigma2'] = np.ones(shape=(n_obs, n_rep)) + test_dict["sensitivity_elements"]["sigma2"] = np.ones(shape=(n_obs, n_rep)) DoubleMLFramework(test_dict) - msg = r'The shape of nu2 does not match the expected shape \(1, 2, 5\)\.' + msg = r"The shape of nu2 does not match the expected shape \(1, 2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['sensitivity_elements']['nu2'] = np.ones(shape=(n_obs, n_rep)) + test_dict["sensitivity_elements"]["nu2"] = np.ones(shape=(n_obs, n_rep)) DoubleMLFramework(test_dict) - msg = r'The shape of psi_sigma2 does not match the expected shape \(10, 2, 5\)\.' + msg = r"The shape of psi_sigma2 does not match the expected shape \(10, 2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['sensitivity_elements']['psi_sigma2'] = np.ones(shape=(n_obs, n_thetas, n_rep, 3)) + test_dict["sensitivity_elements"]["psi_sigma2"] = np.ones(shape=(n_obs, n_thetas, n_rep, 3)) DoubleMLFramework(test_dict) - msg = r'The shape of psi_nu2 does not match the expected shape \(10, 2, 5\)\.' + msg = r"The shape of psi_nu2 does not match the expected shape \(10, 2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['sensitivity_elements']['psi_nu2'] = np.ones(shape=(n_obs, n_thetas, n_rep, 3)) + test_dict["sensitivity_elements"]["psi_nu2"] = np.ones(shape=(n_obs, n_thetas, n_rep, 3)) DoubleMLFramework(test_dict) - msg = r'The shape of riesz_rep does not match the expected shape \(10, 2, 5\)\.' + msg = r"The shape of riesz_rep does not match the expected shape \(10, 2, 5\)\." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['sensitivity_elements']['riesz_rep'] = np.ones(shape=(n_obs, n_thetas, n_rep, 3)) + test_dict["sensitivity_elements"]["riesz_rep"] = np.ones(shape=(n_obs, n_thetas, n_rep, 3)) DoubleMLFramework(test_dict) msg = "is_cluster_data has to be boolean. 1.0 of type was passed." with pytest.raises(TypeError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['is_cluster_data'] = 1.0 + test_dict["is_cluster_data"] = 1.0 DoubleMLFramework(test_dict) msg = "If is_cluster_data is True, cluster_dict must be provided." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['is_cluster_data'] = True + test_dict["is_cluster_data"] = True DoubleMLFramework(test_dict) msg = "cluster_dict must be a dictionary." with pytest.raises(TypeError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['is_cluster_data'] = True - test_dict['cluster_dict'] = 1.0 + test_dict["is_cluster_data"] = True + test_dict["cluster_dict"] = 1.0 DoubleMLFramework(test_dict) - msg = ('The cluster_dict must contain the following keys: smpls, smpls_cluster,' - ' cluster_vars, n_folds_per_cluster. Got: cluster_ids.') + msg = ( + "The cluster_dict must contain the following keys: smpls, smpls_cluster," + " cluster_vars, n_folds_per_cluster. Got: cluster_ids." + ) with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['is_cluster_data'] = True - test_dict['cluster_dict'] = {'cluster_ids': np.ones(shape=(n_obs, n_rep))} + test_dict["is_cluster_data"] = True + test_dict["cluster_dict"] = {"cluster_ids": np.ones(shape=(n_obs, n_rep))} DoubleMLFramework(test_dict) test_dict = copy.deepcopy(doubleml_dict) @@ -150,7 +152,7 @@ def test_input_exceptions(): msg = "treatment_names must be a list. Got 1 of type ." with pytest.raises(TypeError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['treatment_names'] = 1 + test_dict["treatment_names"] = 1 DoubleMLFramework(test_dict) with pytest.raises(TypeError, match=msg): framework_names.treatment_names = 1 @@ -158,18 +160,18 @@ def test_input_exceptions(): msg = r"treatment_names must be a list of strings. At least one element is not a string: \['test', 1\]." with pytest.raises(TypeError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['treatment_names'] = ['test', 1] + test_dict["treatment_names"] = ["test", 1] DoubleMLFramework(test_dict) with pytest.raises(TypeError, match=msg): - framework_names.treatment_names = ['test', 1] + framework_names.treatment_names = ["test", 1] msg = "The length of treatment_names does not match the number of treatments. Got 2 treatments and 3 treatment names." with pytest.raises(ValueError, match=msg): test_dict = copy.deepcopy(doubleml_dict) - test_dict['treatment_names'] = ['test', 'test2', 'test3'] + test_dict["treatment_names"] = ["test", "test2", "test3"] DoubleMLFramework(test_dict) with pytest.raises(ValueError, match=msg): - framework_names.treatment_names = ['test', 'test2', 'test3'] + framework_names.treatment_names = ["test", "test2", "test3"] def test_operation_exceptions(): @@ -179,21 +181,21 @@ def test_operation_exceptions(): _ = dml_framework_obj_1 + 1.0 with pytest.raises(TypeError, match=msg): _ = 1.0 + dml_framework_obj_1 - msg = 'The number of observations in DoubleMLFrameworks must be the same. Got 10 and 11.' + msg = "The number of observations in DoubleMLFrameworks must be the same. Got 10 and 11." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs + 1, n_thetas, n_rep)) psi_b_2 = np.random.normal(size=(n_obs + 1, n_thetas, n_rep)) doubleml_dict_2 = generate_dml_dict(psi_a_2, psi_b_2) dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) _ = dml_framework_obj_1 + dml_framework_obj_2 - msg = 'The number of parameters theta in DoubleMLFrameworks must be the same. Got 2 and 3.' + msg = "The number of parameters theta in DoubleMLFrameworks must be the same. Got 2 and 3." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs, n_thetas + 1, n_rep)) psi_b_2 = np.random.normal(size=(n_obs, n_thetas + 1, n_rep)) doubleml_dict_2 = generate_dml_dict(psi_a_2, psi_b_2) dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) _ = dml_framework_obj_1 + dml_framework_obj_2 - msg = 'The number of replications in DoubleMLFrameworks must be the same. Got 5 and 6.' + msg = "The number of replications in DoubleMLFrameworks must be the same. Got 5 and 6." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs, n_thetas, n_rep + 1)) psi_b_2 = np.random.normal(size=(n_obs, n_thetas, n_rep + 1)) @@ -207,21 +209,21 @@ def test_operation_exceptions(): _ = dml_framework_obj_1 - 1.0 with pytest.raises(TypeError, match=msg): _ = 1.0 - dml_framework_obj_1 - msg = 'The number of observations in DoubleMLFrameworks must be the same. Got 10 and 11.' + msg = "The number of observations in DoubleMLFrameworks must be the same. Got 10 and 11." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs + 1, n_thetas, n_rep)) psi_b_2 = np.random.normal(size=(n_obs + 1, n_thetas, n_rep)) doubleml_dict_2 = generate_dml_dict(psi_a_2, psi_b_2) dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) _ = dml_framework_obj_1 - dml_framework_obj_2 - msg = 'The number of parameters theta in DoubleMLFrameworks must be the same. Got 2 and 3.' + msg = "The number of parameters theta in DoubleMLFrameworks must be the same. Got 2 and 3." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs, n_thetas + 1, n_rep)) psi_b_2 = np.random.normal(size=(n_obs, n_thetas + 1, n_rep)) doubleml_dict_2 = generate_dml_dict(psi_a_2, psi_b_2) dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) _ = dml_framework_obj_1 - dml_framework_obj_2 - msg = 'The number of replications in DoubleMLFrameworks must be the same. Got 5 and 6.' + msg = "The number of replications in DoubleMLFrameworks must be the same. Got 5 and 6." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs, n_thetas, n_rep + 1)) psi_b_2 = np.random.normal(size=(n_obs, n_thetas, n_rep + 1)) @@ -237,20 +239,20 @@ def test_operation_exceptions(): _ = {} * dml_framework_obj_1 # concatenation - msg = 'Need at least one object to concatenate.' + msg = "Need at least one object to concatenate." with pytest.raises(TypeError, match=msg): concat([]) - msg = 'All objects must be of type DoubleMLFramework.' + msg = "All objects must be of type DoubleMLFramework." with pytest.raises(TypeError, match=msg): concat([dml_framework_obj_1, 1.0]) - msg = 'The number of observations in DoubleMLFrameworks must be the same. Got 10 and 11.' + msg = "The number of observations in DoubleMLFrameworks must be the same. Got 10 and 11." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs + 1, n_thetas, n_rep)) psi_b_2 = np.random.normal(size=(n_obs + 1, n_thetas, n_rep)) doubleml_dict_2 = generate_dml_dict(psi_a_2, psi_b_2) dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) _ = concat([dml_framework_obj_1, dml_framework_obj_2]) - msg = 'The number of replications in DoubleMLFrameworks must be the same. Got 5 and 6.' + msg = "The number of replications in DoubleMLFrameworks must be the same. Got 5 and 6." with pytest.raises(ValueError, match=msg): psi_a_2 = np.ones(shape=(n_obs, n_thetas, n_rep + 1)) psi_b_2 = np.random.normal(size=(n_obs, n_thetas, n_rep + 1)) @@ -258,15 +260,15 @@ def test_operation_exceptions(): dml_framework_obj_2 = DoubleMLFramework(doubleml_dict_2) _ = concat([dml_framework_obj_1, dml_framework_obj_2]) - msg = 'concat not yet implemented with clustering.' + msg = "concat not yet implemented with clustering." with pytest.raises(NotImplementedError, match=msg): doubleml_dict_cluster = generate_dml_dict(psi_a_2, psi_b_2) - doubleml_dict_cluster['is_cluster_data'] = True - doubleml_dict_cluster['cluster_dict'] = { - 'smpls': np.ones(shape=(n_obs, n_rep)), - 'smpls_cluster': np.ones(shape=(n_obs, n_rep)), - 'cluster_vars': np.ones(shape=(n_obs, n_rep)), - 'n_folds_per_cluster': 2 + doubleml_dict_cluster["is_cluster_data"] = True + doubleml_dict_cluster["cluster_dict"] = { + "smpls": np.ones(shape=(n_obs, n_rep)), + "smpls_cluster": np.ones(shape=(n_obs, n_rep)), + "cluster_vars": np.ones(shape=(n_obs, n_rep)), + "n_folds_per_cluster": 2, } dml_framework_obj_cluster = DoubleMLFramework(doubleml_dict_cluster) _ = concat([dml_framework_obj_cluster, dml_framework_obj_cluster]) @@ -285,13 +287,13 @@ def test_p_adjust_exceptions(): msg = r'Apply bootstrap\(\) before p_adjust\("rw"\)\.' with pytest.raises(ValueError, match=msg): - _ = dml_framework_obj_1.p_adjust(method='rw') + _ = dml_framework_obj_1.p_adjust(method="rw") @pytest.mark.ci def test_sensitivity_exceptions(): dml_framework_no_sensitivity = DoubleMLFramework(generate_dml_dict(psi_a, psi_b)) - msg = 'Sensitivity analysis is not implemented for this model.' + msg = "Sensitivity analysis is not implemented for this model." with pytest.raises(NotImplementedError, match=msg): _ = dml_framework_no_sensitivity._calc_sensitivity_analysis(cf_y=0.1, cf_d=0.1, rho=1.0, level=0.95) @@ -302,7 +304,7 @@ def test_sensitivity_exceptions(): with pytest.raises(TypeError, match=msg): _ = dml_framework_obj_1._calc_sensitivity_analysis(cf_y=1, cf_d=0.03, rho=1.0, level=0.95) - msg = r'cf_y must be in \[0,1\). 1.0 was passed.' + msg = r"cf_y must be in \[0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_1.sensitivity_analysis(cf_y=1.0) with pytest.raises(ValueError, match=msg): @@ -315,7 +317,7 @@ def test_sensitivity_exceptions(): with pytest.raises(TypeError, match=msg): _ = dml_framework_obj_1._calc_sensitivity_analysis(cf_y=0.1, cf_d=1, rho=1.0, level=0.95) - msg = r'cf_d must be in \[0,1\). 1.0 was passed.' + msg = r"cf_d must be in \[0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_1.sensitivity_analysis(cf_y=0.1, cf_d=1.0) with pytest.raises(ValueError, match=msg): @@ -338,7 +340,7 @@ def test_sensitivity_exceptions(): with pytest.raises(TypeError, match=msg): _ = dml_framework_obj_1._calc_robustness_value(rho="1", null_hypothesis=0.0, level=0.95, idx_treatment=0) - msg = r'The absolute value of rho must be in \[0,1\]. 1.1 was passed.' + msg = r"The absolute value of rho must be in \[0,1\]. 1.1 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_1.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.1) with pytest.raises(ValueError, match=msg): @@ -355,7 +357,7 @@ def test_sensitivity_exceptions(): with pytest.raises(TypeError, match=msg): _ = dml_framework_obj_1._calc_robustness_value(rho=1.0, level=1, null_hypothesis=0.0, idx_treatment=0) - msg = r'The confidence level must be in \(0,1\). 1.0 was passed.' + msg = r"The confidence level must be in \(0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_1.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.0, level=1.0) with pytest.raises(ValueError, match=msg): @@ -363,7 +365,7 @@ def test_sensitivity_exceptions(): with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_1._calc_robustness_value(rho=1.0, level=1.0, null_hypothesis=0.0, idx_treatment=0) - msg = r'The confidence level must be in \(0,1\). 0.0 was passed.' + msg = r"The confidence level must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_1.sensitivity_analysis(cf_y=0.1, cf_d=0.15, rho=1.0, level=0.0) with pytest.raises(ValueError, match=msg): @@ -386,12 +388,12 @@ def test_sensitivity_exceptions(): _ = dml_framework_obj_1._calc_robustness_value(null_hypothesis=np.array([1]), level=0.95, rho=1.0, idx_treatment=0) sensitivity_dict = generate_dml_dict(psi_a, psi_b) - sensitivity_dict['sensitivity_elements'] = { - 'sigma2': np.ones(shape=(1, n_thetas, n_rep)), - 'nu2': -1.0 * np.ones(shape=(1, n_thetas, n_rep)), - 'psi_sigma2': np.ones(shape=(n_obs, n_thetas, n_rep)), - 'psi_nu2': np.ones(shape=(n_obs, n_thetas, n_rep)), - 'riesz_rep': np.ones(shape=(n_obs, n_thetas, n_rep)) + sensitivity_dict["sensitivity_elements"] = { + "sigma2": np.ones(shape=(1, n_thetas, n_rep)), + "nu2": -1.0 * np.ones(shape=(1, n_thetas, n_rep)), + "psi_sigma2": np.ones(shape=(n_obs, n_thetas, n_rep)), + "psi_nu2": np.ones(shape=(n_obs, n_thetas, n_rep)), + "riesz_rep": np.ones(shape=(n_obs, n_thetas, n_rep)), } dml_framework_sensitivity = DoubleMLFramework(sensitivity_dict) @@ -411,10 +413,10 @@ def test_sensitivity_exceptions(): # test variances msg = ( - r'sensitivity_elements sigma2 and nu2 have to be positive\. ' - r'Got sigma2 \[\[\[1\. 1\. 1\. 1\. 1\.\]\n\s+\[1\. 1\. 1\. 1\. 1\.\]\]\] ' - r'and nu2 \[\[\[-1\. -1\. -1\. -1\. -1\.\]\n\s+\[-1\. -1\. -1\. -1\. -1\.\]\]\]\. ' - r'Most likely this is due to low quality learners \(especially propensity scores\)\.' + r"sensitivity_elements sigma2 and nu2 have to be positive\. " + r"Got sigma2 \[\[\[1\. 1\. 1\. 1\. 1\.\]\n\s+\[1\. 1\. 1\. 1\. 1\.\]\]\] " + r"and nu2 \[\[\[-1\. -1\. -1\. -1\. -1\.\]\n\s+\[-1\. -1\. -1\. -1\. -1\.\]\]\]\. " + r"Most likely this is due to low quality learners \(especially propensity scores\)\." ) with pytest.raises(ValueError, match=msg): _ = dml_framework_sensitivity._calc_sensitivity_analysis(cf_y=0.03, cf_d=0.03, rho=1.0, level=0.95) @@ -426,7 +428,7 @@ def test_sensitivity_exceptions(): def test_framework_sensitivity_plot_input(): dml_framework_obj_plot = DoubleMLFramework(doubleml_dict) - msg = (r'Apply sensitivity_analysis\(\) to include senario in sensitivity_plot. ') + msg = r"Apply sensitivity_analysis\(\) to include senario in sensitivity_plot. " with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_plot.sensitivity_plot() @@ -444,30 +446,30 @@ def test_framework_sensitivity_plot_input(): _ = dml_framework_obj_plot.sensitivity_plot(benchmarks="True") msg = r"benchmarks has to be a dictionary with keys cf_y, cf_d and name. Got dict_keys\(\['cf_y', 'cf_d'\]\)." with pytest.raises(ValueError, match=msg): - _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={'cf_y': 0.1, 'cf_d': 0.15}) + _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={"cf_y": 0.1, "cf_d": 0.15}) msg = r"benchmarks has to be a dictionary with values of same length. Got \[1, 2, 2\]." with pytest.raises(ValueError, match=msg): - _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={'cf_y': [0.1], 'cf_d': [0.15, 0.2], - 'name': ['test', 'test2']}) + _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={"cf_y": [0.1], "cf_d": [0.15, 0.2], "name": ["test", "test2"]}) msg = "benchmarks cf_y must be of float type. 2 of type was passed." with pytest.raises(TypeError, match=msg): - _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={'cf_y': [0.1, 2], 'cf_d': [0.15, 0.2], - 'name': ['test', 'test2']}) - msg = r'benchmarks cf_y must be in \[0,1\). 1.0 was passed.' + _ = dml_framework_obj_plot.sensitivity_plot( + benchmarks={"cf_y": [0.1, 2], "cf_d": [0.15, 0.2], "name": ["test", "test2"]} + ) + msg = r"benchmarks cf_y must be in \[0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): - _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={'cf_y': [0.1, 1.0], 'cf_d': [0.15, 0.2], - 'name': ['test', 'test2']}) + _ = dml_framework_obj_plot.sensitivity_plot( + benchmarks={"cf_y": [0.1, 1.0], "cf_d": [0.15, 0.2], "name": ["test", "test2"]} + ) msg = "benchmarks name must be of string type. 2 of type was passed." with pytest.raises(TypeError, match=msg): - _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={'cf_y': [0.1, 0.2], 'cf_d': [0.15, 0.2], - 'name': [2, 2]}) + _ = dml_framework_obj_plot.sensitivity_plot(benchmarks={"cf_y": [0.1, 0.2], "cf_d": [0.15, 0.2], "name": [2, 2]}) msg = "value must be a string. 2 of type was passed." with pytest.raises(TypeError, match=msg): _ = dml_framework_obj_plot.sensitivity_plot(value=2) msg = "Invalid value test. Valid values theta or ci." with pytest.raises(ValueError, match=msg): - _ = dml_framework_obj_plot.sensitivity_plot(value='test') + _ = dml_framework_obj_plot.sensitivity_plot(value="test") msg = "fill has to be boolean. True of type was passed." with pytest.raises(TypeError, match=msg): @@ -485,12 +487,12 @@ def test_framework_sensitivity_plot_input(): _ = dml_framework_obj_plot.sensitivity_plot(grid_bounds=(0.15, 1)) with pytest.raises(TypeError, match=msg): _ = dml_framework_obj_plot.sensitivity_plot(grid_bounds=(1, 0.15)) - msg = r'grid_bounds must be in \(0,1\). 1.0 was passed.' + msg = r"grid_bounds must be in \(0,1\). 1.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_plot.sensitivity_plot(grid_bounds=(1.0, 0.15)) with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_plot.sensitivity_plot(grid_bounds=(0.15, 1.0)) - msg = r'grid_bounds must be in \(0,1\). 0.0 was passed.' + msg = r"grid_bounds must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): _ = dml_framework_obj_plot.sensitivity_plot(grid_bounds=(0.0, 0.15)) with pytest.raises(ValueError, match=msg): diff --git a/doubleml/tests/test_framework_pval_corrections.py b/doubleml/tests/test_framework_pval_corrections.py index 06527defa..b69db44fe 100644 --- a/doubleml/tests/test_framework_pval_corrections.py +++ b/doubleml/tests/test_framework_pval_corrections.py @@ -6,25 +6,22 @@ from ._utils import generate_dml_dict -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 5]) +@pytest.fixture(scope="module", params=[1, 5]) def n_thetas(request): return request.param -@pytest.fixture(scope='module', - params=[0.05, 0.1, 0.2]) +@pytest.fixture(scope="module", params=[0.05, 0.1, 0.2]) def sig_level(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_framework_tstat_pval_fixture(n_rep, n_thetas): n_obs = 100 @@ -35,7 +32,7 @@ def dml_framework_tstat_pval_fixture(n_rep, n_thetas): dml_framework_obj = DoubleMLFramework(doubleml_dict) result_dict = { - 'dml_framework_obj': dml_framework_obj, + "dml_framework_obj": dml_framework_obj, } return result_dict @@ -43,10 +40,10 @@ def dml_framework_tstat_pval_fixture(n_rep, n_thetas): @pytest.mark.ci def test_dml_framework_tstat_shape(dml_framework_tstat_pval_fixture): - dml_framework_obj = dml_framework_tstat_pval_fixture['dml_framework_obj'] + dml_framework_obj = dml_framework_tstat_pval_fixture["dml_framework_obj"] t_stats = dml_framework_obj.t_stats - assert dml_framework_obj.t_stats.shape == (dml_framework_obj.n_thetas, ) + assert dml_framework_obj.t_stats.shape == (dml_framework_obj.n_thetas,) assert np.all(np.isfinite(t_stats)) all_t_stats = dml_framework_obj.all_t_stats @@ -56,10 +53,10 @@ def test_dml_framework_tstat_shape(dml_framework_tstat_pval_fixture): @pytest.mark.ci def test_dml_framework_pval_shape(dml_framework_tstat_pval_fixture): - dml_framework_obj = dml_framework_tstat_pval_fixture['dml_framework_obj'] + dml_framework_obj = dml_framework_tstat_pval_fixture["dml_framework_obj"] p_vals = dml_framework_obj.pvals - assert p_vals.shape == (dml_framework_obj.n_thetas, ) + assert p_vals.shape == (dml_framework_obj.n_thetas,) assert np.all(np.isfinite(p_vals)) all_p_vals = dml_framework_obj.all_pvals @@ -67,7 +64,7 @@ def test_dml_framework_pval_shape(dml_framework_tstat_pval_fixture): assert np.all(np.isfinite(all_p_vals)) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_framework_pval_cov_fixture(n_rep, sig_level): np.random.seed(42) n_thetas = 10 @@ -95,25 +92,25 @@ def dml_framework_pval_cov_fixture(n_rep, sig_level): # p_value corrections # bonferroni - p_vals_bonf, _ = dml_framework_obj.p_adjust(method='bonferroni') - type1_error_bonf[i] = any(p_vals_bonf['pval'] < sig_level) + p_vals_bonf, _ = dml_framework_obj.p_adjust(method="bonferroni") + type1_error_bonf[i] = any(p_vals_bonf["pval"] < sig_level) # holm - p_vals_holm, _ = dml_framework_obj.p_adjust(method='holm') - type1_error_holm[i] = any(p_vals_holm['pval'] < sig_level) + p_vals_holm, _ = dml_framework_obj.p_adjust(method="holm") + type1_error_holm[i] = any(p_vals_holm["pval"] < sig_level) # romano-wolf dml_framework_obj.bootstrap(n_rep_boot=1000) - p_vals_rw, _ = dml_framework_obj.p_adjust(method='romano-wolf') - type1_error_rw[i] = any(p_vals_rw['pval'] < sig_level) + p_vals_rw, _ = dml_framework_obj.p_adjust(method="romano-wolf") + type1_error_rw[i] = any(p_vals_rw["pval"] < sig_level) result_dict = { - 'sig_level': sig_level, - 'avg_type1_error_single_estimate': np.mean(avg_type1_error_single_estimate), - 'avg_type1_error_all_single_estimate': np.mean(avg_type1_error_all_single_estimate), - 'FWER_bonf': np.mean(type1_error_bonf), - 'FWER_holm': np.mean(type1_error_holm), - 'FWER_rw': np.mean(type1_error_rw), + "sig_level": sig_level, + "avg_type1_error_single_estimate": np.mean(avg_type1_error_single_estimate), + "avg_type1_error_all_single_estimate": np.mean(avg_type1_error_all_single_estimate), + "FWER_bonf": np.mean(type1_error_bonf), + "FWER_holm": np.mean(type1_error_holm), + "FWER_rw": np.mean(type1_error_rw), } return result_dict @@ -121,22 +118,23 @@ def dml_framework_pval_cov_fixture(n_rep, sig_level): @pytest.mark.ci def test_dml_framework_pval_FWER(dml_framework_pval_cov_fixture): - sig_level = dml_framework_pval_cov_fixture['sig_level'] - avg_type1_error_single_estimate = dml_framework_pval_cov_fixture['avg_type1_error_single_estimate'] - avg_type1_error_all_single_estimate = dml_framework_pval_cov_fixture['avg_type1_error_all_single_estimate'] + sig_level = dml_framework_pval_cov_fixture["sig_level"] + avg_type1_error_single_estimate = dml_framework_pval_cov_fixture["avg_type1_error_single_estimate"] + avg_type1_error_all_single_estimate = dml_framework_pval_cov_fixture["avg_type1_error_all_single_estimate"] tolerance = 0.02 # only one-sided since median aggregation over independent data assert avg_type1_error_single_estimate <= sig_level + tolerance - assert (sig_level - tolerance <= avg_type1_error_all_single_estimate) & \ - (avg_type1_error_all_single_estimate <= sig_level + tolerance) + assert (sig_level - tolerance <= avg_type1_error_all_single_estimate) & ( + avg_type1_error_all_single_estimate <= sig_level + tolerance + ) # test FWER control - FWER_bonf = dml_framework_pval_cov_fixture['FWER_bonf'] + FWER_bonf = dml_framework_pval_cov_fixture["FWER_bonf"] assert FWER_bonf <= sig_level + tolerance - FWER_holm = dml_framework_pval_cov_fixture['FWER_holm'] + FWER_holm = dml_framework_pval_cov_fixture["FWER_holm"] assert FWER_holm <= sig_level + tolerance - FWER_rw = dml_framework_pval_cov_fixture['FWER_rw'] + FWER_rw = dml_framework_pval_cov_fixture["FWER_rw"] assert FWER_rw <= sig_level + tolerance diff --git a/doubleml/tests/test_framework_sensitivity.py b/doubleml/tests/test_framework_sensitivity.py index e102eb2d5..209b72c14 100644 --- a/doubleml/tests/test_framework_sensitivity.py +++ b/doubleml/tests/test_framework_sensitivity.py @@ -5,13 +5,12 @@ from doubleml.irm.irm import DoubleMLIRM -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_framework_sensitivity_fixture(n_rep, generate_data_simple): dml_data, dml_data_2 = generate_data_simple @@ -39,67 +38,69 @@ def dml_framework_sensitivity_fixture(n_rep, generate_data_simple): dml_framework_obj_concat = concat([dml_framework_obj, dml_framework_obj]) result_dict = { - 'dml_obj': dml_irm_obj, - 'dml_obj_2': dml_irm_obj_2, - 'dml_framework_obj': dml_framework_obj, - 'dml_framework_obj_2': dml_framework_obj_2, - 'dml_framework_obj_add_obj': dml_framework_obj_add_obj, - 'dml_framework_obj_sub_obj': dml_framework_obj_sub_obj, - 'dml_framework_obj_mul_obj': dml_framework_obj_mul_obj, - 'dml_framework_obj_concat': dml_framework_obj_concat, - 'n_rep': n_rep, + "dml_obj": dml_irm_obj, + "dml_obj_2": dml_irm_obj_2, + "dml_framework_obj": dml_framework_obj, + "dml_framework_obj_2": dml_framework_obj_2, + "dml_framework_obj_add_obj": dml_framework_obj_add_obj, + "dml_framework_obj_sub_obj": dml_framework_obj_sub_obj, + "dml_framework_obj_mul_obj": dml_framework_obj_mul_obj, + "dml_framework_obj_concat": dml_framework_obj_concat, + "n_rep": n_rep, } return result_dict @pytest.mark.ci def test_dml_framework_sensitivity_shapes(dml_framework_sensitivity_fixture): - n_rep = dml_framework_sensitivity_fixture['dml_framework_obj'].n_rep - n_thetas = dml_framework_sensitivity_fixture['dml_framework_obj'].n_thetas - n_obs = dml_framework_sensitivity_fixture['dml_framework_obj'].n_obs - - object_list = ['dml_framework_obj', - 'dml_framework_obj_2', - 'dml_framework_obj_add_obj', - 'dml_framework_obj_sub_obj', - 'dml_framework_obj_mul_obj'] - var_keys = ['sigma2', 'nu2'] - score_keys = ['psi_sigma2', 'psi_nu2', 'riesz_rep'] + n_rep = dml_framework_sensitivity_fixture["dml_framework_obj"].n_rep + n_thetas = dml_framework_sensitivity_fixture["dml_framework_obj"].n_thetas + n_obs = dml_framework_sensitivity_fixture["dml_framework_obj"].n_obs + + object_list = [ + "dml_framework_obj", + "dml_framework_obj_2", + "dml_framework_obj_add_obj", + "dml_framework_obj_sub_obj", + "dml_framework_obj_mul_obj", + ] + var_keys = ["sigma2", "nu2"] + score_keys = ["psi_sigma2", "psi_nu2", "riesz_rep"] for obj in object_list: assert dml_framework_sensitivity_fixture[obj]._sensitivity_implemented for key in var_keys: - assert dml_framework_sensitivity_fixture[obj]._sensitivity_elements[key].shape == \ - (1, n_thetas, n_rep) + assert dml_framework_sensitivity_fixture[obj]._sensitivity_elements[key].shape == (1, n_thetas, n_rep) for key in score_keys: - assert dml_framework_sensitivity_fixture[obj]._sensitivity_elements[key].shape == \ - (n_obs, n_thetas, n_rep) + assert dml_framework_sensitivity_fixture[obj]._sensitivity_elements[key].shape == (n_obs, n_thetas, n_rep) # separate test for concat for key in var_keys: - assert dml_framework_sensitivity_fixture['dml_framework_obj_concat']._sensitivity_elements[key].shape == \ - (1, 2, n_rep) + assert dml_framework_sensitivity_fixture["dml_framework_obj_concat"]._sensitivity_elements[key].shape == (1, 2, n_rep) for key in score_keys: - assert dml_framework_sensitivity_fixture['dml_framework_obj_concat']._sensitivity_elements[key].shape == \ - (n_obs, 2, n_rep) + assert dml_framework_sensitivity_fixture["dml_framework_obj_concat"]._sensitivity_elements[key].shape == ( + n_obs, + 2, + n_rep, + ) @pytest.mark.ci def test_dml_framework_sensitivity_summary(dml_framework_sensitivity_fixture): # summary without sensitivity analysis - sensitivity_summary = dml_framework_sensitivity_fixture['dml_framework_obj_2'].sensitivity_summary - substring = 'Apply sensitivity_analysis() to generate sensitivity_summary.' + sensitivity_summary = dml_framework_sensitivity_fixture["dml_framework_obj_2"].sensitivity_summary + substring = "Apply sensitivity_analysis() to generate sensitivity_summary." assert substring in sensitivity_summary # summary with sensitivity analysis - sensitivity_summary = dml_framework_sensitivity_fixture['dml_framework_obj'].sensitivity_summary + sensitivity_summary = dml_framework_sensitivity_fixture["dml_framework_obj"].sensitivity_summary assert isinstance(sensitivity_summary, str) substrings = [ - '\n------------------ Scenario ------------------\n', - '\n------------------ Bounds with CI ------------------\n', - '\n------------------ Robustness Values ------------------\n', - 'Significance Level: level=', - 'Sensitivity parameters: cf_y=' + "\n------------------ Scenario ------------------\n", + "\n------------------ Bounds with CI ------------------\n", + "\n------------------ Robustness Values ------------------\n", + "Significance Level: level=", + "Sensitivity parameters: cf_y=", ] for substring in substrings: assert substring in sensitivity_summary diff --git a/doubleml/tests/test_model_defaults.py b/doubleml/tests/test_model_defaults.py index a592fcb26..401827b19 100644 --- a/doubleml/tests/test_model_defaults.py +++ b/doubleml/tests/test_model_defaults.py @@ -63,14 +63,14 @@ def _assert_resampling_default_settings(dml_obj): assert dml_obj.models is None # bootstrap method - assert dml_obj.boot_method == 'normal' + assert dml_obj.boot_method == "normal" assert dml_obj.n_rep_boot == 500 # confint method assert dml_obj.confint().equals(dml_obj.confint(joint=False, level=0.95)) # p_adjust method - assert dml_obj.p_adjust().equals(dml_obj.p_adjust(method='romano-wolf')) + assert dml_obj.p_adjust().equals(dml_obj.p_adjust(method="romano-wolf")) @pytest.mark.ci @@ -78,7 +78,7 @@ def test_plr_defaults(): _assert_is_none(dml_plr) _fit_bootstrap(dml_plr) _assert_resampling_default_settings(dml_plr) - assert dml_plr.score == 'partialling out' + assert dml_plr.score == "partialling out" @pytest.mark.ci @@ -86,7 +86,7 @@ def test_pliv_defaults(): _assert_is_none(dml_pliv) _fit_bootstrap(dml_pliv) _assert_resampling_default_settings(dml_pliv) - assert dml_pliv.score == 'partialling out' + assert dml_pliv.score == "partialling out" assert dml_pliv.partialX assert not dml_pliv.partialZ @@ -96,12 +96,12 @@ def test_irm_defaults(): _assert_is_none(dml_irm) _fit_bootstrap(dml_irm) _assert_resampling_default_settings(dml_irm) - assert dml_irm.score == 'ATE' - assert dml_irm.trimming_rule == 'truncate' + assert dml_irm.score == "ATE" + assert dml_irm.trimming_rule == "truncate" assert dml_irm.trimming_threshold == 1e-2 assert not dml_irm.normalize_ipw - assert set(dml_irm.weights.keys()) == set(['weights']) - assert np.array_equal(dml_irm.weights['weights'], np.ones((dml_irm._dml_data.n_obs,))) + assert set(dml_irm.weights.keys()) == set(["weights"]) + assert np.array_equal(dml_irm.weights["weights"], np.ones((dml_irm._dml_data.n_obs,))) @pytest.mark.ci @@ -109,9 +109,9 @@ def test_iivm_defaults(): _assert_is_none(dml_iivm) _fit_bootstrap(dml_iivm) _assert_resampling_default_settings(dml_iivm) - assert dml_iivm.score == 'LATE' - assert dml_iivm.subgroups == {'always_takers': True, 'never_takers': True} - assert dml_iivm.trimming_rule == 'truncate' + assert dml_iivm.score == "LATE" + assert dml_iivm.subgroups == {"always_takers": True, "never_takers": True} + assert dml_iivm.trimming_rule == "truncate" assert dml_iivm.trimming_threshold == 1e-2 assert not dml_iivm.normalize_ipw @@ -123,8 +123,8 @@ def test_cvar_defaults(): _assert_resampling_default_settings(dml_cvar) assert dml_cvar.quantile == 0.5 assert dml_cvar.treatment == 1 - assert dml_cvar.score == 'CVaR' - assert dml_cvar.trimming_rule == 'truncate' + assert dml_cvar.score == "CVaR" + assert dml_cvar.trimming_rule == "truncate" assert dml_cvar.trimming_threshold == 1e-2 @@ -135,8 +135,8 @@ def test_pq_defaults(): _assert_resampling_default_settings(dml_pq) assert dml_pq.quantile == 0.5 assert dml_pq.treatment == 1 - assert dml_pq.score == 'PQ' - assert dml_pq.trimming_rule == 'truncate' + assert dml_pq.score == "PQ" + assert dml_pq.trimming_rule == "truncate" assert dml_pq.trimming_threshold == 1e-2 assert dml_pq.normalize_ipw @@ -148,8 +148,8 @@ def test_lpq_defaults(): _assert_resampling_default_settings(dml_lpq) assert dml_lpq.quantile == 0.5 assert dml_lpq.treatment == 1 - assert dml_lpq.score == 'LPQ' - assert dml_lpq.trimming_rule == 'truncate' + assert dml_lpq.score == "LPQ" + assert dml_lpq.trimming_rule == "truncate" assert dml_lpq.trimming_threshold == 1e-2 assert dml_lpq.normalize_ipw @@ -163,8 +163,8 @@ def test_qte_defaults(): _fit_bootstrap(dml_qte) # not fix since its a differen object added in future versions _assert_resampling_default_settings(dml_qte) assert dml_qte.quantiles == 0.5 - assert dml_qte.score == 'PQ' - assert dml_qte.trimming_rule == 'truncate' + assert dml_qte.score == "PQ" + assert dml_qte.trimming_rule == "truncate" assert dml_qte.trimming_threshold == 1e-2 assert dml_qte.normalize_ipw @@ -174,9 +174,9 @@ def test_did_defaults(): _assert_is_none(dml_did) _fit_bootstrap(dml_did) _assert_resampling_default_settings(dml_did) - assert dml_did.score == 'observational' + assert dml_did.score == "observational" assert dml_did.in_sample_normalization - assert dml_did.trimming_rule == 'truncate' + assert dml_did.trimming_rule == "truncate" assert dml_did.trimming_threshold == 1e-2 @@ -185,9 +185,9 @@ def test_did_cs_defaults(): _assert_is_none(dml_did_cs) _fit_bootstrap(dml_did_cs) _assert_resampling_default_settings(dml_did_cs) - assert dml_did.score == 'observational' + assert dml_did.score == "observational" assert dml_did_cs.in_sample_normalization - assert dml_did_cs.trimming_rule == 'truncate' + assert dml_did_cs.trimming_rule == "truncate" assert dml_did_cs.trimming_threshold == 1e-2 @@ -196,8 +196,8 @@ def test_ssm_defaults(): _assert_is_none(dml_ssm) _fit_bootstrap(dml_ssm) _assert_resampling_default_settings(dml_ssm) - assert dml_ssm.score == 'missing-at-random' - assert dml_ssm.trimming_rule == 'truncate' + assert dml_ssm.score == "missing-at-random" + assert dml_ssm.trimming_rule == "truncate" assert dml_ssm.trimming_threshold == 1e-2 assert not dml_ssm.normalize_ipw @@ -207,12 +207,12 @@ def test_apo_defaults(): _assert_is_none(dml_apo) _fit_bootstrap(dml_apo) _assert_resampling_default_settings(dml_apo) - assert dml_apo.score == 'APO' - assert dml_apo.trimming_rule == 'truncate' + assert dml_apo.score == "APO" + assert dml_apo.trimming_rule == "truncate" assert dml_apo.trimming_threshold == 1e-2 assert not dml_apo.normalize_ipw - assert set(dml_apo.weights.keys()) == set(['weights']) - assert np.array_equal(dml_apo.weights['weights'], np.ones((dml_apo._dml_data.n_obs,))) + assert set(dml_apo.weights.keys()) == set(["weights"]) + assert np.array_equal(dml_apo.weights["weights"], np.ones((dml_apo._dml_data.n_obs,))) @pytest.mark.ci @@ -222,8 +222,8 @@ def test_apos_defaults(): assert dml_apos.framework is None assert dml_apos.boot_t_stat is None _fit_bootstrap(dml_qte) - assert dml_apos.score == 'APO' - assert dml_apos.trimming_rule == 'truncate' + assert dml_apos.score == "APO" + assert dml_apos.trimming_rule == "truncate" assert dml_apos.trimming_threshold == 1e-2 assert not dml_apos.normalize_ipw assert np.array_equal(dml_apos.weights, np.ones((dml_apos._dml_data.n_obs,))) @@ -231,14 +231,10 @@ def test_apos_defaults(): @pytest.mark.ci def test_sensitivity_defaults(): - input_dict = {'cf_y': 0.03, - 'cf_d': 0.03, - 'rho': 1.0, - 'level': 0.95, - 'null_hypothesis': np.array([0.])} + input_dict = {"cf_y": 0.03, "cf_d": 0.03, "rho": 1.0, "level": 0.95, "null_hypothesis": np.array([0.0])} dml_plr.sensitivity_analysis() - assert dml_plr.sensitivity_params['input'] == input_dict + assert dml_plr.sensitivity_params["input"] == input_dict @pytest.mark.ci diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index 1ffd994a5..2ccbe5b2a 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -20,30 +20,32 @@ obj_dml_cluster_data = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x) -obj_dml_oneway_cluster_data = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x, - omega_X=np.array([0.25, 0]), - omega_epsilon=np.array([0.25, 0]), - omega_v=np.array([0.25, 0]), - omega_V=np.array([0.25, 0])) +obj_dml_oneway_cluster_data = make_pliv_multiway_cluster_CKMS2021( + N, + M, + dim_x, + omega_X=np.array([0.25, 0]), + omega_epsilon=np.array([0.25, 0]), + omega_v=np.array([0.25, 0]), + omega_V=np.array([0.25, 0]), +) # only the first cluster variable is relevant with the weight setting above -obj_dml_oneway_cluster_data.cluster_cols = 'cluster_var_i' +obj_dml_oneway_cluster_data.cluster_cols = "cluster_var_i" -@pytest.fixture(scope='module', - params=[RandomForestRegressor(max_depth=2, n_estimators=10), - LinearRegression(), - Lasso(alpha=0.1)]) +@pytest.fixture( + scope="module", params=[RandomForestRegressor(max_depth=2, n_estimators=10), LinearRegression(), Lasso(alpha=0.1)] +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['partialling out', 'IV-type']) +@pytest.fixture(scope="module", params=["partialling out", "IV-type"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score): n_folds = 2 n_rep = 2 @@ -52,32 +54,22 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score): ml_l = _clone(learner) ml_m = _clone(learner) ml_r = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_cluster_data, - ml_l, ml_m, ml_r, ml_g, - n_folds=n_folds, - n_rep=n_rep, - score=score) + dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_cluster_data, ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, n_rep=n_rep, score=score) np.random.seed(3141) dml_pliv_obj.fit() dml_pliv_obj_ext_smpls = dml.DoubleMLPLIV( - obj_dml_cluster_data, - ml_l, ml_m, ml_r, ml_g, - n_folds=n_folds, - n_rep=n_rep, - score=score, - draw_sample_splitting=False) + obj_dml_cluster_data, ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, n_rep=n_rep, score=score, draw_sample_splitting=False + ) - dml_pliv_obj_ext_smpls.set_sample_splitting( - all_smpls=dml_pliv_obj.smpls, - all_smpls_cluster=dml_pliv_obj.smpls_cluster) + dml_pliv_obj_ext_smpls.set_sample_splitting(all_smpls=dml_pliv_obj.smpls, all_smpls_cluster=dml_pliv_obj.smpls_cluster) np.random.seed(3141) dml_pliv_obj_ext_smpls.fit() @@ -88,45 +80,42 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score): d = obj_dml_cluster_data.d z = np.ravel(obj_dml_cluster_data.z) - res_manual = fit_pliv(y, x, d, z, - _clone(learner), _clone(learner), _clone(learner), _clone(learner), - dml_pliv_obj.smpls, score, - n_rep=n_rep) + res_manual = fit_pliv( + y, x, d, z, _clone(learner), _clone(learner), _clone(learner), _clone(learner), dml_pliv_obj.smpls, score, n_rep=n_rep + ) thetas = np.full(n_rep, np.nan) ses = np.full(n_rep, np.nan) for i_rep in range(n_rep): - l_hat = res_manual['all_l_hat'][i_rep] - m_hat = res_manual['all_m_hat'][i_rep] - r_hat = res_manual['all_r_hat'][i_rep] - g_hat = res_manual['all_g_hat'][i_rep] + l_hat = res_manual["all_l_hat"][i_rep] + m_hat = res_manual["all_m_hat"][i_rep] + r_hat = res_manual["all_r_hat"][i_rep] + g_hat = res_manual["all_g_hat"][i_rep] smpls_one_split = dml_pliv_obj.smpls[i_rep] y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat = compute_pliv_residuals( - y, d, z, l_hat, m_hat, r_hat, g_hat, smpls_one_split) + y, d, z, l_hat, m_hat, r_hat, g_hat, smpls_one_split + ) - if score == 'partialling out': + if score == "partialling out": psi_a = -np.multiply(z_minus_m_hat, d_minus_r_hat) psi_b = np.multiply(z_minus_m_hat, y_minus_l_hat) - theta = est_two_way_cluster_dml2(psi_a, psi_b, - obj_dml_cluster_data.cluster_vars[:, 0], - obj_dml_cluster_data.cluster_vars[:, 1], - smpls_one_split) + theta = est_two_way_cluster_dml2( + psi_a, psi_b, obj_dml_cluster_data.cluster_vars[:, 0], obj_dml_cluster_data.cluster_vars[:, 1], smpls_one_split + ) psi = np.multiply(y_minus_l_hat - d_minus_r_hat * theta, z_minus_m_hat) else: - assert score == 'IV-type' + assert score == "IV-type" psi_a = -np.multiply(z_minus_m_hat, d) psi_b = np.multiply(z_minus_m_hat, y_minus_g_hat) - theta = est_two_way_cluster_dml2(psi_a, psi_b, - obj_dml_cluster_data.cluster_vars[:, 0], - obj_dml_cluster_data.cluster_vars[:, 1], - smpls_one_split) + theta = est_two_way_cluster_dml2( + psi_a, psi_b, obj_dml_cluster_data.cluster_vars[:, 0], obj_dml_cluster_data.cluster_vars[:, 1], smpls_one_split + ) psi = np.multiply(y_minus_g_hat - d * theta, z_minus_m_hat) - var = var_two_way_cluster(psi, psi_a, - obj_dml_cluster_data.cluster_vars[:, 0], - obj_dml_cluster_data.cluster_vars[:, 1], - smpls_one_split) + var = var_two_way_cluster( + psi, psi_a, obj_dml_cluster_data.cluster_vars[:, 0], obj_dml_cluster_data.cluster_vars[:, 1], smpls_one_split + ) se = np.sqrt(var) thetas[i_rep] = theta ses[i_rep] = se[0] @@ -137,37 +126,48 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score): var_scaling_factor = min(n_clusters1, n_clusters2) se = np.sqrt(np.median(np.power(ses, 2) * var_scaling_factor + np.power(thetas - theta, 2)) / var_scaling_factor) - res_dict = {'coef': dml_pliv_obj.coef, - 'se': dml_pliv_obj.se, - 'coef_manual': theta, - 'se_manual': se, - 'coef_ext_smpls': dml_pliv_obj_ext_smpls.coef, - 'se_ext_smpls': dml_pliv_obj_ext_smpls.se} + res_dict = { + "coef": dml_pliv_obj.coef, + "se": dml_pliv_obj.se, + "coef_manual": theta, + "se_manual": se, + "coef_ext_smpls": dml_pliv_obj_ext_smpls.coef, + "se_ext_smpls": dml_pliv_obj_ext_smpls.se, + } return res_dict @pytest.mark.ci def test_dml_pliv_multiway_cluster_coef(dml_pliv_multiway_cluster_fixture): - assert math.isclose(dml_pliv_multiway_cluster_fixture['coef'][0], - dml_pliv_multiway_cluster_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_pliv_multiway_cluster_fixture['coef'][0], - dml_pliv_multiway_cluster_fixture['coef_ext_smpls'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_multiway_cluster_fixture["coef"][0], + dml_pliv_multiway_cluster_fixture["coef_manual"], + rel_tol=1e-9, + abs_tol=1e-4, + ) + assert math.isclose( + dml_pliv_multiway_cluster_fixture["coef"][0], + dml_pliv_multiway_cluster_fixture["coef_ext_smpls"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.mark.ci def test_dml_pliv_multiway_cluster_se(dml_pliv_multiway_cluster_fixture): - assert math.isclose(dml_pliv_multiway_cluster_fixture['se'][0], - dml_pliv_multiway_cluster_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_pliv_multiway_cluster_fixture['se'][0], - dml_pliv_multiway_cluster_fixture['se_ext_smpls'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_multiway_cluster_fixture["se"][0], dml_pliv_multiway_cluster_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) + assert math.isclose( + dml_pliv_multiway_cluster_fixture["se"][0], + dml_pliv_multiway_cluster_fixture["se_ext_smpls"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, score): n_folds = 3 @@ -175,30 +175,22 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, score): ml_l = _clone(learner) ml_m = _clone(learner) ml_r = _clone(learner) - if score == 'IV-type': + if score == "IV-type": ml_g = _clone(learner) else: ml_g = None np.random.seed(3141) - dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_r, ml_g, - n_folds=n_folds, - score=score) + dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_oneway_cluster_data, ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, score=score) np.random.seed(3141) dml_pliv_obj.fit() dml_pliv_obj_ext_smpls = dml.DoubleMLPLIV( - obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_r, ml_g, - n_folds=n_folds, - score=score, - draw_sample_splitting=False) + obj_dml_oneway_cluster_data, ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, score=score, draw_sample_splitting=False + ) - dml_pliv_obj_ext_smpls.set_sample_splitting( - all_smpls=dml_pliv_obj.smpls, - all_smpls_cluster=dml_pliv_obj.smpls_cluster) + dml_pliv_obj_ext_smpls.set_sample_splitting(all_smpls=dml_pliv_obj.smpls, all_smpls_cluster=dml_pliv_obj.smpls_cluster) np.random.seed(3141) dml_pliv_obj_ext_smpls.fit() @@ -209,68 +201,71 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, score): d = obj_dml_oneway_cluster_data.d z = np.ravel(obj_dml_oneway_cluster_data.z) - res_manual = fit_pliv(y, x, d, z, - _clone(learner), _clone(learner), _clone(learner), _clone(learner), - dml_pliv_obj.smpls, score) - l_hat = res_manual['all_l_hat'][0] - m_hat = res_manual['all_m_hat'][0] - r_hat = res_manual['all_r_hat'][0] - g_hat = res_manual['all_g_hat'][0] + res_manual = fit_pliv( + y, x, d, z, _clone(learner), _clone(learner), _clone(learner), _clone(learner), dml_pliv_obj.smpls, score + ) + l_hat = res_manual["all_l_hat"][0] + m_hat = res_manual["all_m_hat"][0] + r_hat = res_manual["all_r_hat"][0] + g_hat = res_manual["all_g_hat"][0] smpls_one_split = dml_pliv_obj.smpls[0] y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat = compute_pliv_residuals( - y, d, z, l_hat, m_hat, r_hat, g_hat, smpls_one_split) + y, d, z, l_hat, m_hat, r_hat, g_hat, smpls_one_split + ) - if score == 'partialling out': + if score == "partialling out": psi_a = -np.multiply(z_minus_m_hat, d_minus_r_hat) psi_b = np.multiply(z_minus_m_hat, y_minus_l_hat) - theta = est_one_way_cluster_dml2(psi_a, psi_b, - obj_dml_oneway_cluster_data.cluster_vars[:, 0], - smpls_one_split) + theta = est_one_way_cluster_dml2(psi_a, psi_b, obj_dml_oneway_cluster_data.cluster_vars[:, 0], smpls_one_split) psi = np.multiply(y_minus_l_hat - d_minus_r_hat * theta, z_minus_m_hat) else: - assert score == 'IV-type' + assert score == "IV-type" psi_a = -np.multiply(z_minus_m_hat, d) psi_b = np.multiply(z_minus_m_hat, y_minus_g_hat) - theta = est_one_way_cluster_dml2(psi_a, psi_b, - obj_dml_oneway_cluster_data.cluster_vars[:, 0], - smpls_one_split) + theta = est_one_way_cluster_dml2(psi_a, psi_b, obj_dml_oneway_cluster_data.cluster_vars[:, 0], smpls_one_split) psi = np.multiply(y_minus_g_hat - d * theta, z_minus_m_hat) - var = var_one_way_cluster(psi, psi_a, - obj_dml_oneway_cluster_data.cluster_vars[:, 0], - smpls_one_split) + var = var_one_way_cluster(psi, psi_a, obj_dml_oneway_cluster_data.cluster_vars[:, 0], smpls_one_split) se = np.sqrt(var) - res_dict = {'coef': dml_pliv_obj.coef, - 'se': dml_pliv_obj.se, - 'coef_manual': theta, - 'se_manual': se, - 'coef_ext_smpls': dml_pliv_obj_ext_smpls.coef, - 'se_ext_smpls': dml_pliv_obj_ext_smpls.se} + res_dict = { + "coef": dml_pliv_obj.coef, + "se": dml_pliv_obj.se, + "coef_manual": theta, + "se_manual": se, + "coef_ext_smpls": dml_pliv_obj_ext_smpls.coef, + "se_ext_smpls": dml_pliv_obj_ext_smpls.se, + } return res_dict @pytest.mark.ci def test_dml_pliv_oneway_cluster_coef(dml_pliv_oneway_cluster_fixture): - assert math.isclose(dml_pliv_oneway_cluster_fixture['coef'][0], - dml_pliv_oneway_cluster_fixture['coef_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_pliv_oneway_cluster_fixture['coef'][0], - dml_pliv_oneway_cluster_fixture['coef_ext_smpls'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_oneway_cluster_fixture["coef"][0], dml_pliv_oneway_cluster_fixture["coef_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) + assert math.isclose( + dml_pliv_oneway_cluster_fixture["coef"][0], + dml_pliv_oneway_cluster_fixture["coef_ext_smpls"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.mark.ci def test_dml_pliv_oneway_cluster_se(dml_pliv_oneway_cluster_fixture): - assert math.isclose(dml_pliv_oneway_cluster_fixture['se'][0], - dml_pliv_oneway_cluster_fixture['se_manual'], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_pliv_oneway_cluster_fixture['se'][0], - dml_pliv_oneway_cluster_fixture['se_ext_smpls'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_pliv_oneway_cluster_fixture["se"][0], dml_pliv_oneway_cluster_fixture["se_manual"], rel_tol=1e-9, abs_tol=1e-4 + ) + assert math.isclose( + dml_pliv_oneway_cluster_fixture["se"][0], + dml_pliv_oneway_cluster_fixture["se_ext_smpls"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.fixture(scope="module") @@ -280,71 +275,61 @@ def dml_plr_cluster_with_index(generate_data1, learner): # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for m & l ml_l = _clone(learner) ml_m = _clone(learner) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) np.random.seed(3141) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, - n_folds=n_folds) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds=n_folds) np.random.seed(3141) dml_plr_obj.fit() df = data.reset_index() - dml_cluster_data = dml.DoubleMLClusterData(df, - y_col='y', - d_cols='d', - x_cols=x_cols, - cluster_cols='index') + dml_cluster_data = dml.DoubleMLClusterData(df, y_col="y", d_cols="d", x_cols=x_cols, cluster_cols="index") np.random.seed(3141) - dml_plr_cluster_obj = dml.DoubleMLPLR(dml_cluster_data, - ml_l, ml_m, - n_folds=n_folds) + dml_plr_cluster_obj = dml.DoubleMLPLR(dml_cluster_data, ml_l, ml_m, n_folds=n_folds) np.random.seed(3141) dml_plr_cluster_obj.fit() - dml_plr_cluster_ext_smpls = dml.DoubleMLPLR( - dml_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - draw_sample_splitting=False) + dml_plr_cluster_ext_smpls = dml.DoubleMLPLR(dml_cluster_data, ml_l, ml_m, n_folds=n_folds, draw_sample_splitting=False) dml_plr_cluster_ext_smpls.set_sample_splitting( - all_smpls=dml_plr_cluster_obj.smpls, - all_smpls_cluster=dml_plr_cluster_obj.smpls_cluster) + all_smpls=dml_plr_cluster_obj.smpls, all_smpls_cluster=dml_plr_cluster_obj.smpls_cluster + ) np.random.seed(3141) dml_plr_cluster_ext_smpls.fit() - res_dict = {'coef': dml_plr_obj.coef, - 'coef_manual': dml_plr_cluster_obj.coef, - 'se': dml_plr_obj.se, - 'se_manual': dml_plr_cluster_obj.se, - 'coef_ext_smpls': dml_plr_cluster_ext_smpls.coef, - 'se_ext_smpls': dml_plr_cluster_ext_smpls.se} + res_dict = { + "coef": dml_plr_obj.coef, + "coef_manual": dml_plr_cluster_obj.coef, + "se": dml_plr_obj.se, + "se_manual": dml_plr_cluster_obj.se, + "coef_ext_smpls": dml_plr_cluster_ext_smpls.coef, + "se_ext_smpls": dml_plr_cluster_ext_smpls.se, + } return res_dict @pytest.mark.ci def test_dml_plr_cluster_with_index_coef(dml_plr_cluster_with_index): - assert math.isclose(dml_plr_cluster_with_index['coef'][0], - dml_plr_cluster_with_index['coef_manual'][0], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_plr_cluster_with_index['coef'][0], - dml_plr_cluster_with_index['coef_ext_smpls'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_cluster_with_index["coef"][0], dml_plr_cluster_with_index["coef_manual"][0], rel_tol=1e-9, abs_tol=1e-4 + ) + assert math.isclose( + dml_plr_cluster_with_index["coef"][0], dml_plr_cluster_with_index["coef_ext_smpls"][0], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_plr_cluster_with_index_se(dml_plr_cluster_with_index): - assert math.isclose(dml_plr_cluster_with_index['se'][0], - dml_plr_cluster_with_index['se_manual'][0], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_plr_cluster_with_index['se'][0], - dml_plr_cluster_with_index['se_ext_smpls'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_cluster_with_index["se"][0], dml_plr_cluster_with_index["se_manual"][0], rel_tol=1e-9, abs_tol=1e-4 + ) + assert math.isclose( + dml_plr_cluster_with_index["se"][0], dml_plr_cluster_with_index["se_ext_smpls"][0], rel_tol=1e-9, abs_tol=1e-4 + ) diff --git a/doubleml/tests/test_nonlinear_cluster.py b/doubleml/tests/test_nonlinear_cluster.py index 2c9555c00..f84f3e2e9 100644 --- a/doubleml/tests/test_nonlinear_cluster.py +++ b/doubleml/tests/test_nonlinear_cluster.py @@ -21,33 +21,35 @@ x, y, d, cluster_vars, z = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x, return_type="array") obj_dml_cluster_data = DoubleMLClusterData.from_arrays(x, y, d, cluster_vars) -x, y, d, cluster_vars, z = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x, - omega_X=np.array([0.25, 0]), - omega_epsilon=np.array([0.25, 0]), - omega_v=np.array([0.25, 0]), - omega_V=np.array([0.25, 0]), - return_type='array') +x, y, d, cluster_vars, z = make_pliv_multiway_cluster_CKMS2021( + N, + M, + dim_x, + omega_X=np.array([0.25, 0]), + omega_epsilon=np.array([0.25, 0]), + omega_v=np.array([0.25, 0]), + omega_V=np.array([0.25, 0]), + return_type="array", +) obj_dml_oneway_cluster_data = DoubleMLClusterData.from_arrays(x, y, d, cluster_vars) # only the first cluster variable is relevant with the weight setting above -obj_dml_oneway_cluster_data.cluster_cols = 'cluster_var1' +obj_dml_oneway_cluster_data.cluster_cols = "cluster_var1" -@pytest.fixture(scope='module', - params=[RandomForestRegressor(max_depth=2, n_estimators=10), - LinearRegression(), - Lasso(alpha=0.1)]) +@pytest.fixture( + scope="module", params=[RandomForestRegressor(max_depth=2, n_estimators=10), LinearRegression(), Lasso(alpha=0.1)] +) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_plr_oneway_cluster_linear_vs_nonlinear_fixture(learner, score): n_folds = 3 @@ -57,60 +59,60 @@ def dml_plr_oneway_cluster_linear_vs_nonlinear_fixture(learner, score): ml_g = clone(learner) np.random.seed(3141) - if score == 'partialling out': - dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + if score == "partialling out": + dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, ml_l, ml_m, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_g, - n_folds=n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, ml_l, ml_m, ml_g, n_folds=n_folds, score=score) np.random.seed(3141) dml_plr_obj.fit() np.random.seed(3141) - if score == 'partialling out': - dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_oneway_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + if score == "partialling out": + dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin( + obj_dml_oneway_cluster_data, ml_l, ml_m, n_folds=n_folds, score=score + ) else: - assert score == 'IV-type' - dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_g, - n_folds=n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin( + obj_dml_oneway_cluster_data, ml_l, ml_m, ml_g, n_folds=n_folds, score=score + ) np.random.seed(3141) dml_plr_obj2.fit() - res_dict = {'coef_linear': dml_plr_obj.coef, - 'coef_nonlinear': dml_plr_obj2.coef, - 'se_linear': dml_plr_obj.se, - 'se_nonlinear': dml_plr_obj2.se} + res_dict = { + "coef_linear": dml_plr_obj.coef, + "coef_nonlinear": dml_plr_obj2.coef, + "se_linear": dml_plr_obj.se, + "se_nonlinear": dml_plr_obj2.se, + } return res_dict @pytest.mark.ci def test_dml_plr_oneway_cluster_linear_vs_nonlinear_coef(dml_plr_oneway_cluster_linear_vs_nonlinear_fixture): - assert math.isclose(dml_plr_oneway_cluster_linear_vs_nonlinear_fixture['coef_linear'][0], - dml_plr_oneway_cluster_linear_vs_nonlinear_fixture['coef_nonlinear'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_oneway_cluster_linear_vs_nonlinear_fixture["coef_linear"][0], + dml_plr_oneway_cluster_linear_vs_nonlinear_fixture["coef_nonlinear"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.mark.ci def test_dml_plr_oneway_cluster_linear_vs_nonlinear_se(dml_plr_oneway_cluster_linear_vs_nonlinear_fixture): - assert math.isclose(dml_plr_oneway_cluster_linear_vs_nonlinear_fixture['se_linear'][0], - dml_plr_oneway_cluster_linear_vs_nonlinear_fixture['se_nonlinear'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_oneway_cluster_linear_vs_nonlinear_fixture["se_linear"][0], + dml_plr_oneway_cluster_linear_vs_nonlinear_fixture["se_nonlinear"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_plr_multiway_cluster_linear_vs_nonlinear_fixture(learner, score): n_folds = 2 n_rep = 2 @@ -121,61 +123,57 @@ def dml_plr_multiway_cluster_linear_vs_nonlinear_fixture(learner, score): ml_g = clone(learner) np.random.seed(3141) - if score == 'partialling out': - dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - n_rep=n_rep, - score=score) + if score == "partialling out": + dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, ml_l, ml_m, n_folds=n_folds, n_rep=n_rep, score=score) else: - assert score == 'IV-type' - dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_g, - n_folds=n_folds, - n_rep=n_rep, - score=score) + assert score == "IV-type" + dml_plr_obj = dml.DoubleMLPLR(obj_dml_oneway_cluster_data, ml_l, ml_m, ml_g, n_folds=n_folds, n_rep=n_rep, score=score) np.random.seed(3141) dml_plr_obj.fit() np.random.seed(3141) - if score == 'partialling out': - dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_oneway_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - n_rep=n_rep, - score=score) + if score == "partialling out": + dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin( + obj_dml_oneway_cluster_data, ml_l, ml_m, n_folds=n_folds, n_rep=n_rep, score=score + ) else: - assert score == 'IV-type' - dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_g, - n_folds=n_folds, - n_rep=n_rep, - score=score) + assert score == "IV-type" + dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin( + obj_dml_oneway_cluster_data, ml_l, ml_m, ml_g, n_folds=n_folds, n_rep=n_rep, score=score + ) np.random.seed(3141) dml_plr_obj2.fit() - res_dict = {'coef_linear': dml_plr_obj.coef, - 'coef_nonlinear': dml_plr_obj2.coef, - 'se_linear': dml_plr_obj.se, - 'se_nonlinear': dml_plr_obj2.se} + res_dict = { + "coef_linear": dml_plr_obj.coef, + "coef_nonlinear": dml_plr_obj2.coef, + "se_linear": dml_plr_obj.se, + "se_nonlinear": dml_plr_obj2.se, + } return res_dict @pytest.mark.ci def test_dml_plr_multiway_cluster_linear_vs_nonlinear_coef(dml_plr_multiway_cluster_linear_vs_nonlinear_fixture): - assert math.isclose(dml_plr_multiway_cluster_linear_vs_nonlinear_fixture['coef_linear'][0], - dml_plr_multiway_cluster_linear_vs_nonlinear_fixture['coef_nonlinear'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_multiway_cluster_linear_vs_nonlinear_fixture["coef_linear"][0], + dml_plr_multiway_cluster_linear_vs_nonlinear_fixture["coef_nonlinear"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.mark.ci def test_dml_plr_multiway_cluster_linear_vs_nonlinear_se(dml_plr_multiway_cluster_linear_vs_nonlinear_fixture): - assert math.isclose(dml_plr_multiway_cluster_linear_vs_nonlinear_fixture['se_linear'][0], - dml_plr_multiway_cluster_linear_vs_nonlinear_fixture['se_nonlinear'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_multiway_cluster_linear_vs_nonlinear_fixture["se_linear"][0], + dml_plr_multiway_cluster_linear_vs_nonlinear_fixture["se_nonlinear"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.fixture(scope="module") @@ -185,48 +183,48 @@ def dml_plr_cluster_nonlinear_with_index(generate_data1, learner): # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for m & l ml_l = clone(learner) ml_m = clone(learner) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) np.random.seed(3141) - dml_plr_obj = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, - ml_l, ml_m, - n_folds=n_folds) + dml_plr_obj = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, ml_l, ml_m, n_folds=n_folds) dml_plr_obj.fit() df = data.reset_index() - dml_cluster_data = dml.DoubleMLClusterData(df, - y_col='y', - d_cols='d', - x_cols=x_cols, - cluster_cols='index') + dml_cluster_data = dml.DoubleMLClusterData(df, y_col="y", d_cols="d", x_cols=x_cols, cluster_cols="index") np.random.seed(3141) - dml_plr_cluster_obj = DoubleMLPLRWithNonLinearScoreMixin(dml_cluster_data, - ml_l, ml_m, - n_folds=n_folds) + dml_plr_cluster_obj = DoubleMLPLRWithNonLinearScoreMixin(dml_cluster_data, ml_l, ml_m, n_folds=n_folds) dml_plr_cluster_obj.fit() - res_dict = {'coef': dml_plr_obj.coef, - 'coef_cluster': dml_plr_cluster_obj.coef, - 'se': dml_plr_obj.se, - 'se_cluster': dml_plr_cluster_obj.se} + res_dict = { + "coef": dml_plr_obj.coef, + "coef_cluster": dml_plr_cluster_obj.coef, + "se": dml_plr_obj.se, + "se_cluster": dml_plr_cluster_obj.se, + } return res_dict @pytest.mark.ci def test_dml_plr_cluster_nonlinear_with_index_coef(dml_plr_cluster_nonlinear_with_index): - assert math.isclose(dml_plr_cluster_nonlinear_with_index['coef'][0], - dml_plr_cluster_nonlinear_with_index['coef_cluster'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_cluster_nonlinear_with_index["coef"][0], + dml_plr_cluster_nonlinear_with_index["coef_cluster"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.mark.ci def test_dml_plr_cluster_nonlinear_with_index_se(dml_plr_cluster_nonlinear_with_index): - assert math.isclose(dml_plr_cluster_nonlinear_with_index['se'][0], - dml_plr_cluster_nonlinear_with_index['se_cluster'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_cluster_nonlinear_with_index["se"][0], + dml_plr_cluster_nonlinear_with_index["se_cluster"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) diff --git a/doubleml/tests/test_nonlinear_score_mixin.py b/doubleml/tests/test_nonlinear_score_mixin.py index 62e834887..eec0e3c08 100644 --- a/doubleml/tests/test_nonlinear_score_mixin.py +++ b/doubleml/tests/test_nonlinear_score_mixin.py @@ -17,125 +17,125 @@ class DoubleMLPLRWithNonLinearScoreMixin(NonLinearScoreMixin, DoubleML): _coef_bounds = (-np.inf, np.inf) _coef_start_val = 3.0 - def __init__(self, - obj_dml_data, - ml_l, - ml_m, - ml_g=None, - n_folds=5, - n_rep=1, - score='partialling out', - draw_sample_splitting=True): - super().__init__(obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting) + def __init__( + self, obj_dml_data, ml_l, ml_m, ml_g=None, n_folds=5, n_rep=1, score="partialling out", draw_sample_splitting=True + ): + super().__init__(obj_dml_data, n_folds, n_rep, score, draw_sample_splitting) self._check_data(self._dml_data) self._check_score(self.score) - _ = self._check_learner(ml_l, 'ml_l', regressor=True, classifier=False) - _ = self._check_learner(ml_m, 'ml_m', regressor=True, classifier=False) - self._learner = {'ml_l': ml_l, 'ml_m': ml_m} - self._predict_method = {'ml_l': 'predict', - 'ml_m': 'predict'} + _ = self._check_learner(ml_l, "ml_l", regressor=True, classifier=False) + _ = self._check_learner(ml_m, "ml_m", regressor=True, classifier=False) + self._learner = {"ml_l": ml_l, "ml_m": ml_m} + self._predict_method = {"ml_l": "predict", "ml_m": "predict"} if ml_g is not None: - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner['ml_g'] = ml_g - self._predict_method['ml_g'] = 'predict' + _ = self._check_learner(ml_g, "ml_g", regressor=True, classifier=False) + self._learner["ml_g"] = ml_g + self._predict_method["ml_g"] = "predict" self._initialize_ml_nuisance_params() @property def _score_element_names(self): - return ['psi_a', 'psi_b'] + return ["psi_a", "psi_b"] def _compute_score(self, psi_elements, coef, inds=None): - psi_a = psi_elements['psi_a'] - psi_b = psi_elements['psi_b'] + psi_a = psi_elements["psi_a"] + psi_b = psi_elements["psi_b"] if inds is not None: psi_a = psi_a[inds] psi_b = psi_b[inds] psi = psi_a * coef + psi_b - if self.score == 'no_root_pos': - psi = np.full_like(psi, coef**2. + 0.05) - elif self.score == 'no_root_neg': - psi = np.full_like(psi, -coef**2. - 0.75) + if self.score == "no_root_pos": + psi = np.full_like(psi, coef**2.0 + 0.05) + elif self.score == "no_root_neg": + psi = np.full_like(psi, -(coef**2.0) - 0.75) return psi def _compute_score_deriv(self, psi_elements, coef, inds=None): - psi_a = psi_elements['psi_a'] + psi_a = psi_elements["psi_a"] if inds is not None: psi_a = psi_a[inds] - if self.score == 'no_root_pos': - psi_a = np.full_like(psi_a, 2. * coef) - elif self.score == 'no_root_neg': - psi_a = np.full_like(psi_a, -2. * coef) + if self.score == "no_root_pos": + psi_a = np.full_like(psi_a, 2.0 * coef) + elif self.score == "no_root_neg": + psi_a = np.full_like(psi_a, -2.0 * coef) return psi_a def _initialize_ml_nuisance_params(self): - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in self._learner} + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in self._learner} def _check_score(self, score): if isinstance(score, str): - valid_score = ['IV-type', 'partialling out', 'no_root_pos', 'no_root_neg'] + valid_score = ["IV-type", "partialling out", "no_root_pos", "no_root_neg"] if score not in valid_score: - raise ValueError('Invalid score ' + score + '. ' + - 'Valid score ' + ' or '.join(valid_score) + '.') + raise ValueError("Invalid score " + score + ". " + "Valid score " + " or ".join(valid_score) + ".") else: if not callable(score): - raise TypeError('score should be either a string or a callable. ' - '%r was passed.' % score) + raise TypeError("score should be either a string or a callable. " "%r was passed." % score) return def _check_data(self, obj_dml_data): pass def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=False): - x, y = check_X_y(self._dml_data.x, self._dml_data.y, - force_all_finite=False) - x, d = check_X_y(x, self._dml_data.d, - force_all_finite=False) + x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) + x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) # nuisance l - l_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_l'), method=self._predict_method['ml_l']) - _check_finite_predictions(l_hat['preds'], self._learner['ml_l'], 'ml_l', smpls) + l_hat = _dml_cv_predict( + self._learner["ml_l"], + x, + y, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_l"), + method=self._predict_method["ml_l"], + ) + _check_finite_predictions(l_hat["preds"], self._learner["ml_l"], "ml_l", smpls) # nuisance m - m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_m'), method=self._predict_method['ml_m']) - _check_finite_predictions(m_hat['preds'], self._learner['ml_m'], 'ml_m', smpls) + m_hat = _dml_cv_predict( + self._learner["ml_m"], + x, + d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_m"), + method=self._predict_method["ml_m"], + ) + _check_finite_predictions(m_hat["preds"], self._learner["ml_m"], "ml_m", smpls) # an estimate of g is obtained for the IV-type score and callable scores - g_hat = {'preds': None, 'targets': None, 'models': None} - if 'ml_g' in self._learner: + g_hat = {"preds": None, "targets": None, "models": None} + if "ml_g" in self._learner: # get an initial estimate for theta using the partialling out score - psi_a = -np.multiply(d - m_hat['preds'], d - m_hat['preds']) - psi_b = np.multiply(d - m_hat['preds'], y - l_hat['preds']) + psi_a = -np.multiply(d - m_hat["preds"], d - m_hat["preds"]) + psi_b = np.multiply(d - m_hat["preds"], y - l_hat["preds"]) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_g'], x, y - theta_initial*d, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g'), method=self._predict_method['ml_g']) - _check_finite_predictions(g_hat['preds'], self._learner['ml_g'], 'ml_g', smpls) - - psi_a, psi_b = self._score_elements(y, d, l_hat['preds'], m_hat['preds'], g_hat['preds'], smpls) - psi_elements = {'psi_a': psi_a, - 'psi_b': psi_b} - preds = {'predictions': {'ml_l': l_hat['preds'], - 'ml_m': m_hat['preds'], - 'ml_g': g_hat['preds']}, - 'targets': {'ml_l': l_hat['targets'], - 'ml_m': m_hat['targets'], - 'ml_g': g_hat['targets']}, - 'models': {'ml_l': l_hat['models'], - 'ml_m': m_hat['models'], - 'ml_g': g_hat['models']}} + g_hat = _dml_cv_predict( + self._learner["ml_g"], + x, + y - theta_initial * d, + smpls=smpls, + n_jobs=n_jobs_cv, + est_params=self._get_params("ml_g"), + method=self._predict_method["ml_g"], + ) + _check_finite_predictions(g_hat["preds"], self._learner["ml_g"], "ml_g", smpls) + + psi_a, psi_b = self._score_elements(y, d, l_hat["preds"], m_hat["preds"], g_hat["preds"], smpls) + psi_elements = {"psi_a": psi_a, "psi_b": psi_b} + preds = { + "predictions": {"ml_l": l_hat["preds"], "ml_m": m_hat["preds"], "ml_g": g_hat["preds"]}, + "targets": {"ml_l": l_hat["targets"], "ml_m": m_hat["targets"], "ml_g": g_hat["targets"]}, + "models": {"ml_l": l_hat["models"], "ml_m": m_hat["models"], "ml_g": g_hat["models"]}, + } return psi_elements, preds @@ -144,42 +144,39 @@ def _score_elements(self, y, d, l_hat, m_hat, g_hat, smpls): u_hat = y - l_hat v_hat = d - m_hat - if self.score == 'IV-type': - psi_a = - np.multiply(v_hat, d) + if self.score == "IV-type": + psi_a = -np.multiply(v_hat, d) psi_b = np.multiply(v_hat, y - g_hat) - elif self.score == 'partialling out': + elif self.score == "partialling out": psi_a = -np.multiply(v_hat, v_hat) psi_b = np.multiply(v_hat, u_hat) else: - assert self.score in ['no_root_pos', 'no_root_neg'] - psi_a = 1. - psi_b = 1. + assert self.score in ["no_root_pos", "no_root_neg"] + psi_a = 1.0 + psi_b = 1.0 return psi_a, psi_b - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): pass def _sensitivity_element_est(self, preds): pass -@pytest.fixture(scope='module', - params=[LinearRegression()]) +@pytest.fixture(scope="module", params=[LinearRegression()]) def learner(request): return request.param -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module', - params=[(-np.inf, np.inf), - (0, 5)]) +@pytest.fixture(scope="module", params=[(-np.inf, np.inf), (0, 5)]) def coef_bounds(request): return request.param @@ -190,7 +187,7 @@ def dml_plr_w_nonlinear_mixin_fixture(generate_data1, learner, score, coef_bound # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() # Set machine learning methods for l, m & g ml_l = clone(learner) @@ -198,80 +195,61 @@ def dml_plr_w_nonlinear_mixin_fixture(generate_data1, learner, score, coef_bound ml_g = clone(learner) np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - if score == 'partialling out': - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) + if score == "partialling out": + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) dml_plr_obj.fit() np.random.seed(3141) - if score == 'partialling out': - dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + if score == "partialling out": + dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, ml_l, ml_m, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, - ml_l, ml_m, ml_g, - n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj2 = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, ml_l, ml_m, ml_g, n_folds, score=score) dml_plr_obj2._coef_bounds = coef_bounds # use different settings to also unit test the solver for bounded problems dml_plr_obj2.fit() - res_dict = {'coef': dml_plr_obj.coef, - 'coef2': dml_plr_obj2.coef, - 'se': dml_plr_obj.se, - 'se2': dml_plr_obj2.se} + res_dict = {"coef": dml_plr_obj.coef, "coef2": dml_plr_obj2.coef, "se": dml_plr_obj.se, "se2": dml_plr_obj2.se} return res_dict @pytest.mark.ci def test_dml_plr_coef(dml_plr_w_nonlinear_mixin_fixture): - assert math.isclose(dml_plr_w_nonlinear_mixin_fixture['coef'][0], - dml_plr_w_nonlinear_mixin_fixture['coef2'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_w_nonlinear_mixin_fixture["coef"][0], dml_plr_w_nonlinear_mixin_fixture["coef2"][0], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_dml_plr_se(dml_plr_w_nonlinear_mixin_fixture): - assert math.isclose(dml_plr_w_nonlinear_mixin_fixture['se'][0], - dml_plr_w_nonlinear_mixin_fixture['se2'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_w_nonlinear_mixin_fixture["se"][0], dml_plr_w_nonlinear_mixin_fixture["se2"][0], rel_tol=1e-9, abs_tol=1e-4 + ) @pytest.mark.ci def test_nonlinear_warnings(generate_data1, coef_bounds): # collect data data = generate_data1 - x_cols = data.columns[data.columns.str.startswith('X')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) + obj_dml_data = dml.DoubleMLData(data, "y", ["d"], x_cols) - dml_plr_obj = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, - LinearRegression(), LinearRegression(), - score='no_root_pos') - msg = 'Could not find a root of the score function.' + dml_plr_obj = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, LinearRegression(), LinearRegression(), score="no_root_pos") + msg = "Could not find a root of the score function." with pytest.warns(UserWarning, match=msg): dml_plr_obj._coef_bounds = coef_bounds dml_plr_obj.fit() - dml_plr_obj = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, - LinearRegression(), LinearRegression(), - score='no_root_neg') - msg = 'Could not find a root of the score function.' + dml_plr_obj = DoubleMLPLRWithNonLinearScoreMixin(obj_dml_data, LinearRegression(), LinearRegression(), score="no_root_neg") + msg = "Could not find a root of the score function." with pytest.warns(UserWarning, match=msg): dml_plr_obj._coef_bounds = coef_bounds dml_plr_obj.fit() diff --git a/doubleml/tests/test_package.py b/doubleml/tests/test_package.py index 5bfb52e5b..86b3bf0ea 100644 --- a/doubleml/tests/test_package.py +++ b/doubleml/tests/test_package.py @@ -4,4 +4,5 @@ @pytest.mark.ci def test_version_is_string(): import doubleml + assert isinstance(doubleml.__version__, str) diff --git a/doubleml/tests/test_return_types.py b/doubleml/tests/test_return_types.py index e8c573609..7330e8ae8 100644 --- a/doubleml/tests/test_return_types.py +++ b/doubleml/tests/test_return_types.py @@ -42,7 +42,7 @@ dml_cluster_data_pliv = make_pliv_multiway_cluster_CKMS2021(N=10, M=10) dml_data_did = make_did_SZ2020(n_obs=n_obs) dml_data_did_cs = make_did_SZ2020(n_obs=n_obs, cross_sectional_data=True) -(x, y, d, t) = make_did_SZ2020(n_obs=n_obs, cross_sectional_data=True, return_type='array') +(x, y, d, t) = make_did_SZ2020(n_obs=n_obs, cross_sectional_data=True, return_type="array") binary_outcome = np.random.binomial(n=1, p=0.5, size=n_obs) dml_data_did_binary_outcome = DoubleMLData.from_arrays(x, binary_outcome, d) dml_data_did_cs_binary_outcome = DoubleMLData.from_arrays(x, binary_outcome, d, t=t) @@ -65,21 +65,25 @@ @pytest.mark.ci -@pytest.mark.parametrize('dml_obj, cls', - [(dml_plr, DoubleMLPLR), - (dml_pliv, DoubleMLPLIV), - (dml_irm, DoubleMLIRM), - (dml_iivm, DoubleMLIIVM), - (dml_pliv_cluster, DoubleMLPLIV), - (dml_cvar, DoubleMLCVAR), - (dml_pq, DoubleMLPQ), - (dml_lpq, DoubleMLLPQ), - (dml_did, DoubleMLDID), - (dml_did_binary_outcome, DoubleMLDID), - (dml_did_cs, DoubleMLDIDCS), - (dml_did_cs_binary_outcome, DoubleMLDIDCS), - (dml_ssm, DoubleMLSSM), - (dml_apo, DoubleMLAPO)]) +@pytest.mark.parametrize( + "dml_obj, cls", + [ + (dml_plr, DoubleMLPLR), + (dml_pliv, DoubleMLPLIV), + (dml_irm, DoubleMLIRM), + (dml_iivm, DoubleMLIIVM), + (dml_pliv_cluster, DoubleMLPLIV), + (dml_cvar, DoubleMLCVAR), + (dml_pq, DoubleMLPQ), + (dml_lpq, DoubleMLLPQ), + (dml_did, DoubleMLDID), + (dml_did_binary_outcome, DoubleMLDID), + (dml_did_cs, DoubleMLDIDCS), + (dml_did_cs_binary_outcome, DoubleMLDIDCS), + (dml_ssm, DoubleMLSSM), + (dml_apo, DoubleMLAPO), + ], +) def test_return_types(dml_obj, cls): # ToDo: A second test case with multiple treatment variables would be helpful assert isinstance(dml_obj.__str__(), str) @@ -100,13 +104,13 @@ def test_return_types(dml_obj, cls): if not dml_obj._is_cluster_data: assert isinstance(dml_obj.p_adjust(), pd.DataFrame) else: - isinstance(dml_obj.p_adjust('bonferroni'), pd.DataFrame) + isinstance(dml_obj.p_adjust("bonferroni"), pd.DataFrame) if isinstance(dml_obj, DoubleMLLPQ): - assert isinstance(dml_obj.get_params('ml_m_z'), dict) + assert isinstance(dml_obj.get_params("ml_m_z"), dict) elif isinstance(dml_obj, DoubleMLSSM): - assert isinstance(dml_obj.get_params('ml_g_d0'), dict) + assert isinstance(dml_obj.get_params("ml_g_d0"), dict) else: - assert isinstance(dml_obj.get_params('ml_m'), dict) + assert isinstance(dml_obj.get_params("ml_m"), dict) assert isinstance(dml_obj._dml_data.__str__(), str) # for the following checks we need additional inputs @@ -120,66 +124,61 @@ def test_return_types(dml_obj, cls): n_obs = 200 n_rep_boot = 314 -plr_obj = DoubleMLPLR(dml_data_plr, Lasso(), LinearSVR(), - n_rep=n_rep, n_folds=n_folds) +plr_obj = DoubleMLPLR(dml_data_plr, Lasso(), LinearSVR(), n_rep=n_rep, n_folds=n_folds) plr_obj.fit(store_models=True) plr_obj.bootstrap(n_rep_boot=n_rep_boot) -pliv_obj = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso(), - n_rep=n_rep, n_folds=n_folds) +pliv_obj = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso(), n_rep=n_rep, n_folds=n_folds) pliv_obj.fit() pliv_obj.bootstrap(n_rep_boot=n_rep_boot) -irm_obj = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - n_rep=n_rep, n_folds=n_folds, trimming_threshold=0.1) +irm_obj = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), n_rep=n_rep, n_folds=n_folds, trimming_threshold=0.1) irm_obj.fit() irm_obj.bootstrap(n_rep_boot=n_rep_boot) -iivm_obj = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - n_rep=n_rep, n_folds=n_folds) +iivm_obj = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), n_rep=n_rep, n_folds=n_folds) iivm_obj.fit() iivm_obj.bootstrap(n_rep_boot=n_rep_boot) -cvar_obj = DoubleMLCVAR(dml_data_irm, ml_g=RandomForestRegressor(), ml_m=RandomForestClassifier(), - n_rep=n_rep, n_folds=n_folds) +cvar_obj = DoubleMLCVAR( + dml_data_irm, ml_g=RandomForestRegressor(), ml_m=RandomForestClassifier(), n_rep=n_rep, n_folds=n_folds +) cvar_obj.fit() cvar_obj.bootstrap(n_rep_boot=n_rep_boot) -pq_obj = DoubleMLPQ(dml_data_irm, ml_g=RandomForestClassifier(), ml_m=RandomForestClassifier(), - n_rep=n_rep, n_folds=n_folds) +pq_obj = DoubleMLPQ(dml_data_irm, ml_g=RandomForestClassifier(), ml_m=RandomForestClassifier(), n_rep=n_rep, n_folds=n_folds) pq_obj.fit() pq_obj.bootstrap(n_rep_boot=n_rep_boot) -lpq_obj = DoubleMLLPQ(dml_data_iivm, ml_g=RandomForestClassifier(), ml_m=RandomForestClassifier(), - n_rep=n_rep, n_folds=n_folds) +lpq_obj = DoubleMLLPQ( + dml_data_iivm, ml_g=RandomForestClassifier(), ml_m=RandomForestClassifier(), n_rep=n_rep, n_folds=n_folds +) lpq_obj.fit() lpq_obj.bootstrap(n_rep_boot=n_rep_boot) -did_obj = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), - n_rep=n_rep, n_folds=n_folds) +did_obj = DoubleMLDID(dml_data_did, Lasso(), LogisticRegression(), n_rep=n_rep, n_folds=n_folds) did_obj.fit() did_obj.bootstrap(n_rep_boot=n_rep_boot) -did_cs_obj = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), - n_rep=n_rep, n_folds=n_folds) +did_cs_obj = DoubleMLDIDCS(dml_data_did_cs, Lasso(), LogisticRegression(), n_rep=n_rep, n_folds=n_folds) did_cs_obj.fit() did_cs_obj.bootstrap(n_rep_boot=n_rep_boot) -ssm_obj = DoubleMLSSM(dml_data_ssm, ml_g=Lasso(), ml_m=LogisticRegression(), ml_pi=LogisticRegression(), - n_rep=n_rep, n_folds=n_folds) +ssm_obj = DoubleMLSSM( + dml_data_ssm, ml_g=Lasso(), ml_m=LogisticRegression(), ml_pi=LogisticRegression(), n_rep=n_rep, n_folds=n_folds +) ssm_obj.fit() ssm_obj.bootstrap(n_rep_boot=n_rep_boot) -apo_obj = DoubleMLAPO(dml_data_irm, Lasso(), LogisticRegression(), treatment_level=0, - n_rep=n_rep, n_folds=n_folds) +apo_obj = DoubleMLAPO(dml_data_irm, Lasso(), LogisticRegression(), treatment_level=0, n_rep=n_rep, n_folds=n_folds) apo_obj.fit() apo_obj.bootstrap(n_rep_boot=n_rep_boot) @pytest.mark.ci -@pytest.mark.parametrize('dml_obj', - [plr_obj, pliv_obj, irm_obj, iivm_obj, cvar_obj, pq_obj, lpq_obj, - did_obj, did_cs_obj, ssm_obj, apo_obj]) +@pytest.mark.parametrize( + "dml_obj", [plr_obj, pliv_obj, irm_obj, iivm_obj, cvar_obj, pq_obj, lpq_obj, did_obj, did_cs_obj, ssm_obj, apo_obj] +) def test_property_types_and_shapes(dml_obj): # not checked: learner, learner_names, params, params_names, score # already checked: summary @@ -201,10 +200,14 @@ def test_property_types_and_shapes(dml_obj): assert dml_obj.boot_t_stat.shape == (n_rep_boot, n_treat, n_rep) assert isinstance(dml_obj.coef, np.ndarray) - assert dml_obj.coef.shape == (n_treat, ) + assert dml_obj.coef.shape == (n_treat,) assert isinstance(dml_obj.psi, np.ndarray) - assert dml_obj.psi.shape == (n_obs, n_rep, n_treat, ) + assert dml_obj.psi.shape == ( + n_obs, + n_rep, + n_treat, + ) assert isinstance(dml_obj.framework, DoubleMLFramework) @@ -212,22 +215,34 @@ def test_property_types_and_shapes(dml_obj): if is_nonlinear: for score_element in dml_obj._score_element_names: assert isinstance(dml_obj.psi_elements[score_element], np.ndarray) - assert dml_obj.psi_elements[score_element].shape == (n_obs, n_rep, n_treat, ) + assert dml_obj.psi_elements[score_element].shape == ( + n_obs, + n_rep, + n_treat, + ) else: - assert isinstance(dml_obj.psi_elements['psi_a'], np.ndarray) - assert dml_obj.psi_elements['psi_a'].shape == (n_obs, n_rep, n_treat, ) - - assert isinstance(dml_obj.psi_elements['psi_b'], np.ndarray) - assert dml_obj.psi_elements['psi_b'].shape == (n_obs, n_rep, n_treat, ) + assert isinstance(dml_obj.psi_elements["psi_a"], np.ndarray) + assert dml_obj.psi_elements["psi_a"].shape == ( + n_obs, + n_rep, + n_treat, + ) + + assert isinstance(dml_obj.psi_elements["psi_b"], np.ndarray) + assert dml_obj.psi_elements["psi_b"].shape == ( + n_obs, + n_rep, + n_treat, + ) assert isinstance(dml_obj.pval, np.ndarray) - assert dml_obj.pval.shape == (n_treat, ) + assert dml_obj.pval.shape == (n_treat,) assert isinstance(dml_obj.se, np.ndarray) - assert dml_obj.se.shape == (n_treat, ) + assert dml_obj.se.shape == (n_treat,) assert isinstance(dml_obj.t_stat, np.ndarray) - assert dml_obj.t_stat.shape == (n_treat, ) + assert dml_obj.t_stat.shape == (n_treat,) assert isinstance(dml_obj._dml_data.binary_treats, pd.Series) assert len(dml_obj._dml_data.binary_treats) == n_treat @@ -245,181 +260,181 @@ def test_property_types_and_shapes(dml_obj): @pytest.mark.ci def test_stored_models(): - assert len(plr_obj.models['ml_l']['d']) == n_rep - assert len(plr_obj.models['ml_m']['d']) == n_rep + assert len(plr_obj.models["ml_l"]["d"]) == n_rep + assert len(plr_obj.models["ml_m"]["d"]) == n_rep - n_folds_each_model = np.array([len(mdl) for mdl in plr_obj.models['ml_l']['d']]) + n_folds_each_model = np.array([len(mdl) for mdl in plr_obj.models["ml_l"]["d"]]) assert np.all(n_folds_each_model == n_folds_each_model[0]) assert n_folds_each_model[0] == n_folds - n_folds_each_model = np.array([len(mdl) for mdl in plr_obj.models['ml_m']['d']]) + n_folds_each_model = np.array([len(mdl) for mdl in plr_obj.models["ml_m"]["d"]]) assert np.all(n_folds_each_model == n_folds_each_model[0]) assert n_folds_each_model[0] == n_folds - assert np.all([isinstance(mdl, plr_obj.learner['ml_l'].__class__) for mdl in plr_obj.models['ml_l']['d'][0]]) - assert np.all([isinstance(mdl, plr_obj.learner['ml_m'].__class__) for mdl in plr_obj.models['ml_m']['d'][0]]) + assert np.all([isinstance(mdl, plr_obj.learner["ml_l"].__class__) for mdl in plr_obj.models["ml_l"]["d"][0]]) + assert np.all([isinstance(mdl, plr_obj.learner["ml_m"].__class__) for mdl in plr_obj.models["ml_m"]["d"][0]]) # extend these tests to more models @pytest.mark.ci def test_stored_predictions(): - assert plr_obj.predictions['ml_l'].shape == (n_obs, n_rep, n_treat) - assert plr_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert plr_obj.predictions["ml_l"].shape == (n_obs, n_rep, n_treat) + assert plr_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) - assert pliv_obj.predictions['ml_l'].shape == (n_obs, n_rep, n_treat) - assert pliv_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) - assert pliv_obj.predictions['ml_r'].shape == (n_obs, n_rep, n_treat) + assert pliv_obj.predictions["ml_l"].shape == (n_obs, n_rep, n_treat) + assert pliv_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) + assert pliv_obj.predictions["ml_r"].shape == (n_obs, n_rep, n_treat) - assert irm_obj.predictions['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert irm_obj.predictions['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert irm_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert irm_obj.predictions["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert irm_obj.predictions["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert irm_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.predictions['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.predictions['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.predictions['ml_r0'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.predictions['ml_r1'].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.predictions["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.predictions["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.predictions["ml_r0"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.predictions["ml_r1"].shape == (n_obs, n_rep, n_treat) - assert cvar_obj.predictions['ml_g'].shape == (n_obs, n_rep, n_treat) - assert cvar_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert cvar_obj.predictions["ml_g"].shape == (n_obs, n_rep, n_treat) + assert cvar_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) - assert pq_obj.predictions['ml_g'].shape == (n_obs, n_rep, n_treat) - assert pq_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert pq_obj.predictions["ml_g"].shape == (n_obs, n_rep, n_treat) + assert pq_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.predictions['ml_g_du_z0'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.predictions['ml_g_du_z1'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.predictions['ml_m_z'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.predictions['ml_m_d_z0'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.predictions['ml_m_d_z1'].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.predictions["ml_g_du_z0"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.predictions["ml_g_du_z1"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.predictions["ml_m_z"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.predictions["ml_m_d_z0"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.predictions["ml_m_d_z1"].shape == (n_obs, n_rep, n_treat) - assert did_obj.predictions['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert did_obj.predictions['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert did_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert did_obj.predictions["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert did_obj.predictions["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert did_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.predictions['ml_g_d0_t0'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.predictions['ml_g_d0_t1'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.predictions['ml_g_d1_t0'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.predictions['ml_g_d1_t1'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.predictions["ml_g_d0_t0"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.predictions["ml_g_d0_t1"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.predictions["ml_g_d1_t0"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.predictions["ml_g_d1_t1"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.predictions['ml_g_d0'].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.predictions['ml_g_d1'].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.predictions['ml_pi'].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.predictions["ml_g_d0"].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.predictions["ml_g_d1"].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.predictions["ml_pi"].shape == (n_obs, n_rep, n_treat) - assert apo_obj.predictions['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert apo_obj.predictions['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert apo_obj.predictions['ml_m'].shape == (n_obs, n_rep, n_treat) + assert apo_obj.predictions["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert apo_obj.predictions["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert apo_obj.predictions["ml_m"].shape == (n_obs, n_rep, n_treat) @pytest.mark.ci def test_stored_nuisance_targets(): - assert plr_obj.nuisance_targets['ml_l'].shape == (n_obs, n_rep, n_treat) - assert plr_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert plr_obj.nuisance_targets["ml_l"].shape == (n_obs, n_rep, n_treat) + assert plr_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) - assert pliv_obj.nuisance_targets['ml_l'].shape == (n_obs, n_rep, n_treat) - assert pliv_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) - assert pliv_obj.nuisance_targets['ml_r'].shape == (n_obs, n_rep, n_treat) + assert pliv_obj.nuisance_targets["ml_l"].shape == (n_obs, n_rep, n_treat) + assert pliv_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) + assert pliv_obj.nuisance_targets["ml_r"].shape == (n_obs, n_rep, n_treat) - assert irm_obj.nuisance_targets['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert irm_obj.nuisance_targets['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert irm_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert irm_obj.nuisance_targets["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert irm_obj.nuisance_targets["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert irm_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.nuisance_targets['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.nuisance_targets['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.nuisance_targets['ml_r0'].shape == (n_obs, n_rep, n_treat) - assert iivm_obj.nuisance_targets['ml_r1'].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.nuisance_targets["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.nuisance_targets["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.nuisance_targets["ml_r0"].shape == (n_obs, n_rep, n_treat) + assert iivm_obj.nuisance_targets["ml_r1"].shape == (n_obs, n_rep, n_treat) - assert cvar_obj.nuisance_targets['ml_g'].shape == (n_obs, n_rep, n_treat) - assert cvar_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert cvar_obj.nuisance_targets["ml_g"].shape == (n_obs, n_rep, n_treat) + assert cvar_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) - assert pq_obj.nuisance_targets['ml_g'].shape == (n_obs, n_rep, n_treat) - assert pq_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert pq_obj.nuisance_targets["ml_g"].shape == (n_obs, n_rep, n_treat) + assert pq_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.nuisance_targets['ml_g_du_z0'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.nuisance_targets['ml_g_du_z1'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.nuisance_targets['ml_m_z'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.nuisance_targets['ml_m_d_z0'].shape == (n_obs, n_rep, n_treat) - assert lpq_obj.nuisance_targets['ml_m_d_z1'].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.nuisance_targets["ml_g_du_z0"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.nuisance_targets["ml_g_du_z1"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.nuisance_targets["ml_m_z"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.nuisance_targets["ml_m_d_z0"].shape == (n_obs, n_rep, n_treat) + assert lpq_obj.nuisance_targets["ml_m_d_z1"].shape == (n_obs, n_rep, n_treat) - assert did_obj.nuisance_targets['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert did_obj.nuisance_targets['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert did_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert did_obj.nuisance_targets["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert did_obj.nuisance_targets["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert did_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.nuisance_targets['ml_g_d0_t0'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.nuisance_targets['ml_g_d0_t1'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.nuisance_targets['ml_g_d1_t0'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.nuisance_targets['ml_g_d1_t1'].shape == (n_obs, n_rep, n_treat) - assert did_cs_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.nuisance_targets["ml_g_d0_t0"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.nuisance_targets["ml_g_d0_t1"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.nuisance_targets["ml_g_d1_t0"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.nuisance_targets["ml_g_d1_t1"].shape == (n_obs, n_rep, n_treat) + assert did_cs_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.nuisance_targets['ml_g_d0'].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.nuisance_targets['ml_g_d1'].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) - assert ssm_obj.nuisance_targets['ml_pi'].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.nuisance_targets["ml_g_d0"].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.nuisance_targets["ml_g_d1"].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) + assert ssm_obj.nuisance_targets["ml_pi"].shape == (n_obs, n_rep, n_treat) - assert apo_obj.nuisance_targets['ml_g0'].shape == (n_obs, n_rep, n_treat) - assert apo_obj.nuisance_targets['ml_g1'].shape == (n_obs, n_rep, n_treat) - assert apo_obj.nuisance_targets['ml_m'].shape == (n_obs, n_rep, n_treat) + assert apo_obj.nuisance_targets["ml_g0"].shape == (n_obs, n_rep, n_treat) + assert apo_obj.nuisance_targets["ml_g1"].shape == (n_obs, n_rep, n_treat) + assert apo_obj.nuisance_targets["ml_m"].shape == (n_obs, n_rep, n_treat) @pytest.mark.ci def test_nuisance_loss(): - assert plr_obj.nuisance_loss['ml_l'].shape == (n_rep, n_treat) - assert plr_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert plr_obj.nuisance_loss["ml_l"].shape == (n_rep, n_treat) + assert plr_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) - assert pliv_obj.nuisance_loss['ml_l'].shape == (n_rep, n_treat) - assert pliv_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) - assert pliv_obj.nuisance_loss['ml_r'].shape == (n_rep, n_treat) + assert pliv_obj.nuisance_loss["ml_l"].shape == (n_rep, n_treat) + assert pliv_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) + assert pliv_obj.nuisance_loss["ml_r"].shape == (n_rep, n_treat) - assert irm_obj.nuisance_loss['ml_g0'].shape == (n_rep, n_treat) - assert irm_obj.nuisance_loss['ml_g1'].shape == (n_rep, n_treat) - assert irm_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert irm_obj.nuisance_loss["ml_g0"].shape == (n_rep, n_treat) + assert irm_obj.nuisance_loss["ml_g1"].shape == (n_rep, n_treat) + assert irm_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) - assert iivm_obj.nuisance_loss['ml_g0'].shape == (n_rep, n_treat) - assert iivm_obj.nuisance_loss['ml_g1'].shape == (n_rep, n_treat) - assert iivm_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) - assert iivm_obj.nuisance_loss['ml_r0'].shape == (n_rep, n_treat) - assert iivm_obj.nuisance_loss['ml_r1'].shape == (n_rep, n_treat) + assert iivm_obj.nuisance_loss["ml_g0"].shape == (n_rep, n_treat) + assert iivm_obj.nuisance_loss["ml_g1"].shape == (n_rep, n_treat) + assert iivm_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) + assert iivm_obj.nuisance_loss["ml_r0"].shape == (n_rep, n_treat) + assert iivm_obj.nuisance_loss["ml_r1"].shape == (n_rep, n_treat) - assert cvar_obj.nuisance_loss['ml_g'].shape == (n_rep, n_treat) - assert cvar_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert cvar_obj.nuisance_loss["ml_g"].shape == (n_rep, n_treat) + assert cvar_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) - assert pq_obj.nuisance_loss['ml_g'].shape == (n_rep, n_treat) - assert pq_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert pq_obj.nuisance_loss["ml_g"].shape == (n_rep, n_treat) + assert pq_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) - assert lpq_obj.nuisance_loss['ml_g_du_z0'].shape == (n_rep, n_treat) - assert lpq_obj.nuisance_loss['ml_g_du_z1'].shape == (n_rep, n_treat) - assert lpq_obj.nuisance_loss['ml_m_z'].shape == (n_rep, n_treat) - assert lpq_obj.nuisance_loss['ml_m_d_z0'].shape == (n_rep, n_treat) - assert lpq_obj.nuisance_loss['ml_m_d_z1'].shape == (n_rep, n_treat) + assert lpq_obj.nuisance_loss["ml_g_du_z0"].shape == (n_rep, n_treat) + assert lpq_obj.nuisance_loss["ml_g_du_z1"].shape == (n_rep, n_treat) + assert lpq_obj.nuisance_loss["ml_m_z"].shape == (n_rep, n_treat) + assert lpq_obj.nuisance_loss["ml_m_d_z0"].shape == (n_rep, n_treat) + assert lpq_obj.nuisance_loss["ml_m_d_z1"].shape == (n_rep, n_treat) - assert did_obj.nuisance_loss['ml_g0'].shape == (n_rep, n_treat) - assert did_obj.nuisance_loss['ml_g1'].shape == (n_rep, n_treat) - assert did_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert did_obj.nuisance_loss["ml_g0"].shape == (n_rep, n_treat) + assert did_obj.nuisance_loss["ml_g1"].shape == (n_rep, n_treat) + assert did_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) - assert did_cs_obj.nuisance_loss['ml_g_d0_t0'].shape == (n_rep, n_treat) - assert did_cs_obj.nuisance_loss['ml_g_d0_t1'].shape == (n_rep, n_treat) - assert did_cs_obj.nuisance_loss['ml_g_d1_t0'].shape == (n_rep, n_treat) - assert did_cs_obj.nuisance_loss['ml_g_d1_t1'].shape == (n_rep, n_treat) - assert did_cs_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert did_cs_obj.nuisance_loss["ml_g_d0_t0"].shape == (n_rep, n_treat) + assert did_cs_obj.nuisance_loss["ml_g_d0_t1"].shape == (n_rep, n_treat) + assert did_cs_obj.nuisance_loss["ml_g_d1_t0"].shape == (n_rep, n_treat) + assert did_cs_obj.nuisance_loss["ml_g_d1_t1"].shape == (n_rep, n_treat) + assert did_cs_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) - assert ssm_obj.nuisance_loss['ml_g_d0'].shape == (n_rep, n_treat) - assert ssm_obj.nuisance_loss['ml_g_d1'].shape == (n_rep, n_treat) - assert ssm_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) - assert ssm_obj.nuisance_loss['ml_pi'].shape == (n_rep, n_treat) + assert ssm_obj.nuisance_loss["ml_g_d0"].shape == (n_rep, n_treat) + assert ssm_obj.nuisance_loss["ml_g_d1"].shape == (n_rep, n_treat) + assert ssm_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) + assert ssm_obj.nuisance_loss["ml_pi"].shape == (n_rep, n_treat) - assert apo_obj.nuisance_loss['ml_g0'].shape == (n_rep, n_treat) - assert apo_obj.nuisance_loss['ml_g1'].shape == (n_rep, n_treat) - assert apo_obj.nuisance_loss['ml_m'].shape == (n_rep, n_treat) + assert apo_obj.nuisance_loss["ml_g0"].shape == (n_rep, n_treat) + assert apo_obj.nuisance_loss["ml_g1"].shape == (n_rep, n_treat) + assert apo_obj.nuisance_loss["ml_m"].shape == (n_rep, n_treat) def _test_sensitivity_return_types(dml_obj, n_rep, n_treat, benchmarking_set): assert isinstance(dml_obj.sensitivity_elements, dict) - for key in ['sigma2', 'nu2']: + for key in ["sigma2", "nu2"]: assert isinstance(dml_obj.sensitivity_elements[key], np.ndarray) assert dml_obj.sensitivity_elements[key].shape == (1, n_rep, n_treat) - for key in ['psi_sigma2', 'psi_nu2', 'riesz_rep']: + for key in ["psi_sigma2", "psi_nu2", "riesz_rep"]: assert isinstance(dml_obj.sensitivity_elements[key], np.ndarray) assert dml_obj.sensitivity_elements[key].shape == (n_obs, n_rep, n_treat) @@ -427,13 +442,12 @@ def _test_sensitivity_return_types(dml_obj, n_rep, n_treat, benchmarking_set): dml_obj.sensitivity_analysis() assert isinstance(dml_obj.sensitivity_summary, str) assert isinstance(dml_obj.sensitivity_plot(), plotly.graph_objs._figure.Figure) - benchmarks = {'cf_y': [0.1, 0.2], 'cf_d': [0.15, 0.2], 'name': ["test1", "test2"]} - assert isinstance(dml_obj.sensitivity_plot(value='ci', benchmarks=benchmarks), plotly.graph_objs._figure.Figure) + benchmarks = {"cf_y": [0.1, 0.2], "cf_d": [0.15, 0.2], "name": ["test1", "test2"]} + assert isinstance(dml_obj.sensitivity_plot(value="ci", benchmarks=benchmarks), plotly.graph_objs._figure.Figure) assert isinstance(dml_obj.framework._calc_sensitivity_analysis(cf_y=0.03, cf_d=0.03, rho=1.0, level=0.95), dict) assert isinstance( - dml_obj.framework._calc_robustness_value(null_hypothesis=0.0, level=0.95, rho=1.0, idx_treatment=0), - tuple + dml_obj.framework._calc_robustness_value(null_hypothesis=0.0, level=0.95, rho=1.0, idx_treatment=0), tuple ) benchmark = dml_obj.sensitivity_benchmark(benchmarking_set=benchmarking_set) assert isinstance(benchmark, pd.DataFrame) diff --git a/doubleml/tests/test_scores.py b/doubleml/tests/test_scores.py index 19b554190..c3281702d 100644 --- a/doubleml/tests/test_scores.py +++ b/doubleml/tests/test_scores.py @@ -13,7 +13,7 @@ dml_plr = DoubleMLPLR(dml_data_plr, Lasso(), Lasso()) dml_plr.fit() -dml_plr_iv_type = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), Lasso(), score='IV-type') +dml_plr_iv_type = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), Lasso(), score="IV-type") dml_plr_iv_type.fit() dml_pliv = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso()) dml_pliv.fit() @@ -24,26 +24,34 @@ # fit models with callable scores plr_score = dml_plr._score_elements -dml_plr_callable_score = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), - score=plr_score, draw_sample_splitting=False) +dml_plr_callable_score = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), score=plr_score, draw_sample_splitting=False) dml_plr_callable_score.set_sample_splitting(dml_plr.smpls) dml_plr_callable_score.fit(store_predictions=True) plr_iv_type_score = dml_plr_iv_type._score_elements -dml_plr_iv_type_callable_score = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), Lasso(), - score=plr_iv_type_score, draw_sample_splitting=False) +dml_plr_iv_type_callable_score = DoubleMLPLR( + dml_data_plr, Lasso(), Lasso(), Lasso(), score=plr_iv_type_score, draw_sample_splitting=False +) dml_plr_iv_type_callable_score.set_sample_splitting(dml_plr_iv_type.smpls) dml_plr_iv_type_callable_score.fit(store_predictions=True) irm_score = dml_irm._score_elements -dml_irm_callable_score = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), - score=irm_score, draw_sample_splitting=False, normalize_ipw=False) +dml_irm_callable_score = DoubleMLIRM( + dml_data_irm, Lasso(), LogisticRegression(), score=irm_score, draw_sample_splitting=False, normalize_ipw=False +) dml_irm_callable_score.set_sample_splitting(dml_irm.smpls) dml_irm_callable_score.fit(store_predictions=True) iivm_score = dml_iivm._score_elements -dml_iivm_callable_score = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), - score=iivm_score, draw_sample_splitting=False, normalize_ipw=False) +dml_iivm_callable_score = DoubleMLIIVM( + dml_data_iivm, + Lasso(), + LogisticRegression(), + LogisticRegression(), + score=iivm_score, + draw_sample_splitting=False, + normalize_ipw=False, +) dml_iivm_callable_score.set_sample_splitting(dml_iivm.smpls) dml_iivm_callable_score.fit(store_predictions=True) @@ -67,181 +75,135 @@ def non_orth_score_w_l(y, d, l_hat, m_hat, g_hat, smpls): return psi_a, psi_b -dml_plr_non_orth_score_w_g = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), Lasso(), - score=non_orth_score_w_g) +dml_plr_non_orth_score_w_g = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), Lasso(), score=non_orth_score_w_g) dml_plr_non_orth_score_w_g.fit(store_predictions=True) -dml_plr_non_orth_score_w_l = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), - score=non_orth_score_w_l) +dml_plr_non_orth_score_w_l = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), score=non_orth_score_w_l) dml_plr_non_orth_score_w_l.fit(store_predictions=True) @pytest.mark.ci -@pytest.mark.parametrize('dml_obj', - [dml_plr, dml_plr_iv_type, dml_pliv, dml_irm, dml_iivm]) +@pytest.mark.parametrize("dml_obj", [dml_plr, dml_plr_iv_type, dml_pliv, dml_irm, dml_iivm]) def test_linear_score(dml_obj): - assert np.allclose(dml_obj.psi, - dml_obj.psi_elements['psi_a'] * dml_obj.coef + dml_obj.psi_elements['psi_b'], - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_obj.psi, dml_obj.psi_elements["psi_a"] * dml_obj.coef + dml_obj.psi_elements["psi_b"], rtol=1e-9, atol=1e-4 + ) @pytest.mark.ci def test_plr_callable_vs_str_score(): - assert np.allclose(dml_plr.psi, - dml_plr_callable_score.psi, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr.coef, - dml_plr_callable_score.coef, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr.psi, dml_plr_callable_score.psi, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr.coef, dml_plr_callable_score.coef, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_plr_callable_vs_pred_export(): preds = dml_plr_callable_score.predictions - l_hat = preds['ml_l'].squeeze() - m_hat = preds['ml_m'].squeeze() + l_hat = preds["ml_l"].squeeze() + m_hat = preds["ml_m"].squeeze() g_hat = None - psi_a, psi_b = plr_score(dml_data_plr.y, dml_data_plr.d, - l_hat, m_hat, g_hat, - dml_plr_callable_score.smpls[0]) - assert np.allclose(dml_plr.psi_elements['psi_a'].squeeze(), - psi_a, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr.psi_elements['psi_b'].squeeze(), - psi_b, - rtol=1e-9, atol=1e-4) + psi_a, psi_b = plr_score(dml_data_plr.y, dml_data_plr.d, l_hat, m_hat, g_hat, dml_plr_callable_score.smpls[0]) + assert np.allclose(dml_plr.psi_elements["psi_a"].squeeze(), psi_a, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr.psi_elements["psi_b"].squeeze(), psi_b, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_plr_iv_type_callable_vs_str_score(): - assert np.allclose(dml_plr_iv_type.psi, - dml_plr_iv_type_callable_score.psi, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_iv_type.coef, - dml_plr_iv_type_callable_score.coef, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_iv_type.psi, dml_plr_iv_type_callable_score.psi, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_iv_type.coef, dml_plr_iv_type_callable_score.coef, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_plr_iv_type_callable_vs_pred_export(): preds = dml_plr_iv_type_callable_score.predictions - l_hat = preds['ml_l'].squeeze() - m_hat = preds['ml_m'].squeeze() - g_hat = preds['ml_g'].squeeze() - psi_a, psi_b = plr_iv_type_score(dml_data_plr.y, dml_data_plr.d, - l_hat, m_hat, g_hat, - dml_plr_iv_type_callable_score.smpls[0]) - assert np.allclose(dml_plr_iv_type.psi_elements['psi_a'].squeeze(), - psi_a, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_iv_type.psi_elements['psi_b'].squeeze(), - psi_b, - rtol=1e-9, atol=1e-4) + l_hat = preds["ml_l"].squeeze() + m_hat = preds["ml_m"].squeeze() + g_hat = preds["ml_g"].squeeze() + psi_a, psi_b = plr_iv_type_score( + dml_data_plr.y, dml_data_plr.d, l_hat, m_hat, g_hat, dml_plr_iv_type_callable_score.smpls[0] + ) + assert np.allclose(dml_plr_iv_type.psi_elements["psi_a"].squeeze(), psi_a, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_iv_type.psi_elements["psi_b"].squeeze(), psi_b, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_plr_non_orth_score_w_g_callable_vs_pred_export(): preds = dml_plr_non_orth_score_w_g.predictions - l_hat = preds['ml_l'].squeeze() - m_hat = preds['ml_m'].squeeze() - g_hat = preds['ml_g'].squeeze() - psi_a, psi_b = non_orth_score_w_g(dml_data_plr.y, dml_data_plr.d, - l_hat, m_hat, g_hat, - dml_plr_non_orth_score_w_g.smpls[0]) - assert np.allclose(dml_plr_non_orth_score_w_g.psi_elements['psi_a'].squeeze(), - psi_a, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_non_orth_score_w_g.psi_elements['psi_b'].squeeze(), - psi_b, - rtol=1e-9, atol=1e-4) + l_hat = preds["ml_l"].squeeze() + m_hat = preds["ml_m"].squeeze() + g_hat = preds["ml_g"].squeeze() + psi_a, psi_b = non_orth_score_w_g(dml_data_plr.y, dml_data_plr.d, l_hat, m_hat, g_hat, dml_plr_non_orth_score_w_g.smpls[0]) + assert np.allclose(dml_plr_non_orth_score_w_g.psi_elements["psi_a"].squeeze(), psi_a, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_non_orth_score_w_g.psi_elements["psi_b"].squeeze(), psi_b, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_plr_non_orth_score_w_l_callable_vs_pred_export(): preds = dml_plr_non_orth_score_w_l.predictions - l_hat = preds['ml_l'].squeeze() - m_hat = preds['ml_m'].squeeze() + l_hat = preds["ml_l"].squeeze() + m_hat = preds["ml_m"].squeeze() g_hat = None - psi_a, psi_b = non_orth_score_w_l(dml_data_plr.y, dml_data_plr.d, - l_hat, m_hat, g_hat, - dml_plr_non_orth_score_w_l.smpls[0]) - assert np.allclose(dml_plr_non_orth_score_w_l.psi_elements['psi_a'].squeeze(), - psi_a, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_plr_non_orth_score_w_l.psi_elements['psi_b'].squeeze(), - psi_b, - rtol=1e-9, atol=1e-4) + psi_a, psi_b = non_orth_score_w_l(dml_data_plr.y, dml_data_plr.d, l_hat, m_hat, g_hat, dml_plr_non_orth_score_w_l.smpls[0]) + assert np.allclose(dml_plr_non_orth_score_w_l.psi_elements["psi_a"].squeeze(), psi_a, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_non_orth_score_w_l.psi_elements["psi_b"].squeeze(), psi_b, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_irm_callable_vs_str_score(): - assert np.allclose(dml_irm.psi, - dml_irm_callable_score.psi, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_irm.coef, - dml_irm_callable_score.coef, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_irm.psi, dml_irm_callable_score.psi, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_irm.coef, dml_irm_callable_score.coef, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_irm_callable_vs_pred_export(): preds = dml_irm_callable_score.predictions - g_hat0 = preds['ml_g0'].squeeze() - g_hat1 = preds['ml_g1'].squeeze() - m_hat = preds['ml_m'].squeeze() - psi_a, psi_b = irm_score(dml_data_irm.y, dml_data_irm.d, - g_hat0, g_hat1, m_hat, - dml_irm_callable_score.smpls[0]) - assert np.allclose(dml_irm.psi_elements['psi_a'].squeeze(), - psi_a, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_irm.psi_elements['psi_b'].squeeze(), - psi_b, - rtol=1e-9, atol=1e-4) + g_hat0 = preds["ml_g0"].squeeze() + g_hat1 = preds["ml_g1"].squeeze() + m_hat = preds["ml_m"].squeeze() + psi_a, psi_b = irm_score(dml_data_irm.y, dml_data_irm.d, g_hat0, g_hat1, m_hat, dml_irm_callable_score.smpls[0]) + assert np.allclose(dml_irm.psi_elements["psi_a"].squeeze(), psi_a, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_irm.psi_elements["psi_b"].squeeze(), psi_b, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_iivm_callable_vs_str_score(): - assert np.allclose(dml_iivm.psi, - dml_iivm_callable_score.psi, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_iivm.coef, - dml_iivm_callable_score.coef, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_iivm.psi, dml_iivm_callable_score.psi, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_iivm.coef, dml_iivm_callable_score.coef, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_iivm_callable_vs_pred_export(): preds = dml_iivm_callable_score.predictions - g_hat0 = preds['ml_g0'].squeeze() - g_hat1 = preds['ml_g1'].squeeze() - m_hat = preds['ml_m'].squeeze() - r_hat0 = preds['ml_r0'].squeeze() - r_hat1 = preds['ml_r1'].squeeze() - psi_a, psi_b = iivm_score(dml_data_iivm.y, dml_data_iivm.z.squeeze(), dml_data_iivm.d, - g_hat0, g_hat1, m_hat, r_hat0, r_hat1, - dml_iivm_callable_score.smpls[0]) - assert np.allclose(dml_iivm.psi_elements['psi_a'].squeeze(), - psi_a, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_iivm.psi_elements['psi_b'].squeeze(), - psi_b, - rtol=1e-9, atol=1e-4) + g_hat0 = preds["ml_g0"].squeeze() + g_hat1 = preds["ml_g1"].squeeze() + m_hat = preds["ml_m"].squeeze() + r_hat0 = preds["ml_r0"].squeeze() + r_hat1 = preds["ml_r1"].squeeze() + psi_a, psi_b = iivm_score( + dml_data_iivm.y, + dml_data_iivm.z.squeeze(), + dml_data_iivm.d, + g_hat0, + g_hat1, + m_hat, + r_hat0, + r_hat1, + dml_iivm_callable_score.smpls[0], + ) + assert np.allclose(dml_iivm.psi_elements["psi_a"].squeeze(), psi_a, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_iivm.psi_elements["psi_b"].squeeze(), psi_b, rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_pliv_callable_vs_str_score(): pliv_score = dml_pliv._score_elements - dml_pliv_callable_score = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso(), - score=pliv_score, draw_sample_splitting=False) + dml_pliv_callable_score = DoubleMLPLIV( + dml_data_pliv, Lasso(), Lasso(), Lasso(), score=pliv_score, draw_sample_splitting=False + ) dml_pliv_callable_score.set_sample_splitting(dml_pliv.smpls) dml_pliv_callable_score.fit() - assert np.allclose(dml_pliv.psi, - dml_pliv_callable_score.psi, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_pliv.coef, - dml_pliv_callable_score.coef, - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_pliv.psi, dml_pliv_callable_score.psi, rtol=1e-9, atol=1e-4) + assert np.allclose(dml_pliv.coef, dml_pliv_callable_score.coef, rtol=1e-9, atol=1e-4) @pytest.mark.ci @@ -250,20 +212,17 @@ def test_pliv_callable_not_implemented(): dml_data_pliv_2z = make_pliv_CHS2015(n_obs=100, dim_z=2) pliv_score = dml_pliv._score_elements - dml_pliv_callable_score = DoubleMLPLIV._partialX(dml_data_pliv_2z, Lasso(), Lasso(), Lasso(), - score=pliv_score) - msg = 'Callable score not implemented for DoubleMLPLIV.partialX with several instruments.' + dml_pliv_callable_score = DoubleMLPLIV._partialX(dml_data_pliv_2z, Lasso(), Lasso(), Lasso(), score=pliv_score) + msg = "Callable score not implemented for DoubleMLPLIV.partialX with several instruments." with pytest.raises(NotImplementedError, match=msg): dml_pliv_callable_score.fit() - dml_pliv_callable_score = DoubleMLPLIV._partialZ(dml_data_pliv_2z, Lasso(), - score=pliv_score) - msg = 'Callable score not implemented for DoubleMLPLIV.partialZ.' + dml_pliv_callable_score = DoubleMLPLIV._partialZ(dml_data_pliv_2z, Lasso(), score=pliv_score) + msg = "Callable score not implemented for DoubleMLPLIV.partialZ." with pytest.raises(NotImplementedError, match=msg): dml_pliv_callable_score.fit() - dml_pliv_callable_score = DoubleMLPLIV._partialXZ(dml_data_pliv_2z, Lasso(), Lasso(), Lasso(), - score=pliv_score) - msg = 'Callable score not implemented for DoubleMLPLIV.partialXZ.' + dml_pliv_callable_score = DoubleMLPLIV._partialXZ(dml_data_pliv_2z, Lasso(), Lasso(), Lasso(), score=pliv_score) + msg = "Callable score not implemented for DoubleMLPLIV.partialXZ." with pytest.raises(NotImplementedError, match=msg): dml_pliv_callable_score.fit() diff --git a/doubleml/tests/test_sensitivity.py b/doubleml/tests/test_sensitivity.py index f5008d0bb..4f70c6945 100644 --- a/doubleml/tests/test_sensitivity.py +++ b/doubleml/tests/test_sensitivity.py @@ -16,32 +16,27 @@ def benchmarking_set(request): return request.param -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[0.03, 0.3]) +@pytest.fixture(scope="module", params=[0.03, 0.3]) def cf_y(request): return request.param -@pytest.fixture(scope='module', - params=[0.03, 0.3]) +@pytest.fixture(scope="module", params=[0.03, 0.3]) def cf_d(request): return request.param -@pytest.fixture(scope='module', - params=[-0.5, 0.0, 1.0]) +@pytest.fixture(scope="module", params=[-0.5, 0.0, 1.0]) def rho(request): return request.param -@pytest.fixture(scope='module', - params=[0.8, 0.95]) +@pytest.fixture(scope="module", params=[0.8, 0.95]) def level(request): return request.param @@ -51,62 +46,60 @@ def dml_sensitivity_multitreat_fixture(generate_data_bivariate, n_rep, cf_y, cf_ # collect data data = generate_data_bivariate - x_cols = data.columns[data.columns.str.startswith('X')].tolist() - d_cols = data.columns[data.columns.str.startswith('d')].tolist() + x_cols = data.columns[data.columns.str.startswith("X")].tolist() + d_cols = data.columns[data.columns.str.startswith("d")].tolist() # Set machine learning methods for m & g ml_l = LinearRegression() ml_m = LinearRegression() np.random.seed(3141) - obj_dml_data = dml.DoubleMLData(data, 'y', d_cols, x_cols) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_l, - ml_m, - n_folds=5, - n_rep=n_rep, - score='partialling out') + obj_dml_data = dml.DoubleMLData(data, "y", d_cols, x_cols) + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, n_folds=5, n_rep=n_rep, score="partialling out") dml_plr_obj.fit() dml_plr_obj.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, rho=rho, level=level, null_hypothesis=0.0) - res_manual = doubleml_sensitivity_manual(sensitivity_elements=dml_plr_obj.sensitivity_elements, - all_coefs=dml_plr_obj.all_coef, - psi=dml_plr_obj.psi, - psi_deriv=dml_plr_obj.psi_deriv, - cf_y=cf_y, - cf_d=cf_d, - rho=rho, - level=level) + res_manual = doubleml_sensitivity_manual( + sensitivity_elements=dml_plr_obj.sensitivity_elements, + all_coefs=dml_plr_obj.all_coef, + psi=dml_plr_obj.psi, + psi_deriv=dml_plr_obj.psi_deriv, + cf_y=cf_y, + cf_d=cf_d, + rho=rho, + level=level, + ) benchmark = dml_plr_obj.sensitivity_benchmark(benchmarking_set=["X1"]) - benchmark_manual = doubleml_sensitivity_benchmark_manual(dml_obj=dml_plr_obj, - benchmarking_set=["X1"]) - res_dict = {'sensitivity_params': dml_plr_obj.sensitivity_params, - 'sensitivity_params_manual': res_manual, - 'benchmark': benchmark, - 'benchmark_manual': benchmark_manual, - 'd_cols': d_cols, - } + benchmark_manual = doubleml_sensitivity_benchmark_manual(dml_obj=dml_plr_obj, benchmarking_set=["X1"]) + res_dict = { + "sensitivity_params": dml_plr_obj.sensitivity_params, + "sensitivity_params_manual": res_manual, + "benchmark": benchmark, + "benchmark_manual": benchmark_manual, + "d_cols": d_cols, + } return res_dict @pytest.mark.ci def test_dml_sensitivity_params(dml_sensitivity_multitreat_fixture): - sensitivity_param_names = ['theta', 'se', 'ci'] + sensitivity_param_names = ["theta", "se", "ci"] for sensitivity_param in sensitivity_param_names: - for bound in ['lower', 'upper']: - assert np.allclose(dml_sensitivity_multitreat_fixture['sensitivity_params'][sensitivity_param][bound], - dml_sensitivity_multitreat_fixture['sensitivity_params_manual'][sensitivity_param][bound]) + for bound in ["lower", "upper"]: + assert np.allclose( + dml_sensitivity_multitreat_fixture["sensitivity_params"][sensitivity_param][bound], + dml_sensitivity_multitreat_fixture["sensitivity_params_manual"][sensitivity_param][bound], + ) @pytest.mark.ci def test_dml_sensitivity_benchmark(dml_sensitivity_multitreat_fixture): expected_columns = ["cf_y", "cf_d", "rho", "delta_theta"] - assert all(dml_sensitivity_multitreat_fixture['benchmark'].columns == expected_columns) - assert all(dml_sensitivity_multitreat_fixture['benchmark'].index == - dml_sensitivity_multitreat_fixture['d_cols']) - assert dml_sensitivity_multitreat_fixture['benchmark'].equals(dml_sensitivity_multitreat_fixture['benchmark_manual']) + assert all(dml_sensitivity_multitreat_fixture["benchmark"].columns == expected_columns) + assert all(dml_sensitivity_multitreat_fixture["benchmark"].index == dml_sensitivity_multitreat_fixture["d_cols"]) + assert dml_sensitivity_multitreat_fixture["benchmark"].equals(dml_sensitivity_multitreat_fixture["benchmark_manual"]) @pytest.fixture(scope="module") @@ -120,11 +113,9 @@ def test_dml_benchmark_fixture(benchmarking_set, n_rep): np.random.seed(3141) dml_data = DoubleMLData.from_arrays(x=x, y=y, d=d) x_list_long = copy.deepcopy(dml_data.x_cols) - dml_int = DoubleMLIRM(dml_data, - ml_m=classifier_class(random_state=random_state), - ml_g=regressor_class(), - n_folds=2, - n_rep=n_rep) + dml_int = DoubleMLIRM( + dml_data, ml_m=classifier_class(random_state=random_state), ml_g=regressor_class(), n_folds=2, n_rep=n_rep + ) dml_int.fit(store_predictions=True) dml_int.sensitivity_analysis() dml_ext = copy.deepcopy(dml_int) @@ -133,30 +124,29 @@ def test_dml_benchmark_fixture(benchmarking_set, n_rep): np.random.seed(3141) dml_data_short = DoubleMLData.from_arrays(x=x, y=y, d=d) dml_data_short.x_cols = [x for x in x_list_long if x not in benchmarking_set] - dml_short = DoubleMLIRM(dml_data_short, - ml_m=classifier_class(random_state=random_state), - ml_g=regressor_class(), - n_folds=2, - n_rep=n_rep) + dml_short = DoubleMLIRM( + dml_data_short, ml_m=classifier_class(random_state=random_state), ml_g=regressor_class(), n_folds=2, n_rep=n_rep + ) dml_short.fit(store_predictions=True) - fit_args = {"external_predictions": {"d": {"ml_m": dml_short.predictions["ml_m"][:, :, 0], - "ml_g0": dml_short.predictions["ml_g0"][:, :, 0], - "ml_g1": dml_short.predictions["ml_g1"][:, :, 0], - } - }, - } + fit_args = { + "external_predictions": { + "d": { + "ml_m": dml_short.predictions["ml_m"][:, :, 0], + "ml_g0": dml_short.predictions["ml_g0"][:, :, 0], + "ml_g1": dml_short.predictions["ml_g1"][:, :, 0], + } + }, + } dml_ext.sensitivity_analysis() df_bm_ext = dml_ext.sensitivity_benchmark(benchmarking_set=benchmarking_set, fit_args=fit_args) - res_dict = {"default_benchmark": df_bm, - "external_benchmark": df_bm_ext} + res_dict = {"default_benchmark": df_bm, "external_benchmark": df_bm_ext} return res_dict @pytest.mark.ci def test_dml_sensitivity_external_predictions(test_dml_benchmark_fixture): - assert np.allclose(test_dml_benchmark_fixture["default_benchmark"], - test_dml_benchmark_fixture["external_benchmark"], - rtol=1e-9, - atol=1e-4) + assert np.allclose( + test_dml_benchmark_fixture["default_benchmark"], test_dml_benchmark_fixture["external_benchmark"], rtol=1e-9, atol=1e-4 + ) diff --git a/doubleml/tests/test_sensitivity_cluster.py b/doubleml/tests/test_sensitivity_cluster.py index 20e7d635f..65ec0d644 100644 --- a/doubleml/tests/test_sensitivity_cluster.py +++ b/doubleml/tests/test_sensitivity_cluster.py @@ -16,27 +16,30 @@ dim_x = 10 # dimension of x -(x, y, d, cluster_vars, z) = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x, return_type='array') +(x, y, d, cluster_vars, z) = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x, return_type="array") obj_dml_cluster_data = dml.DoubleMLClusterData.from_arrays(x, y, d, cluster_vars) -(x, y, d, cluster_vars, z) = make_pliv_multiway_cluster_CKMS2021(N, M, dim_x, - omega_X=np.array([0.25, 0]), - omega_epsilon=np.array([0.25, 0]), - omega_v=np.array([0.25, 0]), - omega_V=np.array([0.25, 0]), - return_type='array') +(x, y, d, cluster_vars, z) = make_pliv_multiway_cluster_CKMS2021( + N, + M, + dim_x, + omega_X=np.array([0.25, 0]), + omega_epsilon=np.array([0.25, 0]), + omega_v=np.array([0.25, 0]), + omega_V=np.array([0.25, 0]), + return_type="array", +) obj_dml_oneway_cluster_data = dml.DoubleMLClusterData.from_arrays(x, y, d, cluster_vars) # only the first cluster variable is relevant with the weight setting above -obj_dml_oneway_cluster_data.cluster_cols = 'cluster_var1' +obj_dml_oneway_cluster_data.cluster_cols = "cluster_var1" -@pytest.fixture(scope='module', - params=['IV-type', 'partialling out']) +@pytest.fixture(scope="module", params=["IV-type", "partialling out"]) def score(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_plr_multiway_cluster_sensitivity_rho0(score): n_folds = 3 cf_y = 0.03 @@ -49,30 +52,22 @@ def dml_plr_multiway_cluster_sensitivity_rho0(score): ml_g = LinearRegression() np.random.seed(3141) - if score == 'partialling out': - dml_plr_obj = dml.DoubleMLPLR(obj_dml_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - score=score) + if score == "partialling out": + dml_plr_obj = dml.DoubleMLPLR(obj_dml_cluster_data, ml_l, ml_m, n_folds=n_folds, score=score) else: - assert score == 'IV-type' - dml_plr_obj = dml.DoubleMLPLR(obj_dml_cluster_data, - ml_l, ml_m, ml_g, - n_folds=n_folds, - score=score) + assert score == "IV-type" + dml_plr_obj = dml.DoubleMLPLR(obj_dml_cluster_data, ml_l, ml_m, ml_g, n_folds=n_folds, score=score) dml_plr_obj.fit() - dml_plr_obj.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, - rho=0.0, level=level, null_hypothesis=0.0) + dml_plr_obj.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, rho=0.0, level=level, null_hypothesis=0.0) benchmark = dml_plr_obj.sensitivity_benchmark(benchmarking_set=["X1"]) - benchmark_manual = doubleml_sensitivity_benchmark_manual(dml_obj=dml_plr_obj, - benchmarking_set=["X1"]) + benchmark_manual = doubleml_sensitivity_benchmark_manual(dml_obj=dml_plr_obj, benchmarking_set=["X1"]) res_dict = { - 'coef': dml_plr_obj.coef, - 'se': dml_plr_obj.se, - 'sensitivity_params': dml_plr_obj.sensitivity_params, - 'benchmark': benchmark, - 'benchmark_manual': benchmark_manual + "coef": dml_plr_obj.coef, + "se": dml_plr_obj.se, + "sensitivity_params": dml_plr_obj.sensitivity_params, + "benchmark": benchmark, + "benchmark_manual": benchmark_manual, } return res_dict @@ -80,24 +75,31 @@ def dml_plr_multiway_cluster_sensitivity_rho0(score): @pytest.mark.ci def test_dml_plr_multiway_cluster_sensitivity_coef(dml_plr_multiway_cluster_sensitivity_rho0): - assert math.isclose(dml_plr_multiway_cluster_sensitivity_rho0['coef'][0], - dml_plr_multiway_cluster_sensitivity_rho0['sensitivity_params']['theta']['lower'][0], - rel_tol=1e-9, abs_tol=1e-4) - assert math.isclose(dml_plr_multiway_cluster_sensitivity_rho0['coef'][0], - dml_plr_multiway_cluster_sensitivity_rho0['sensitivity_params']['theta']['upper'][0], - rel_tol=1e-9, abs_tol=1e-4) + assert math.isclose( + dml_plr_multiway_cluster_sensitivity_rho0["coef"][0], + dml_plr_multiway_cluster_sensitivity_rho0["sensitivity_params"]["theta"]["lower"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) + assert math.isclose( + dml_plr_multiway_cluster_sensitivity_rho0["coef"][0], + dml_plr_multiway_cluster_sensitivity_rho0["sensitivity_params"]["theta"]["upper"][0], + rel_tol=1e-9, + abs_tol=1e-4, + ) @pytest.mark.ci def test_dml_sensitivity_benchmark(dml_plr_multiway_cluster_sensitivity_rho0): expected_columns = ["cf_y", "cf_d", "rho", "delta_theta"] - assert all(dml_plr_multiway_cluster_sensitivity_rho0['benchmark'].columns == expected_columns) - assert all(dml_plr_multiway_cluster_sensitivity_rho0['benchmark'].index == ["d"]) - assert dml_plr_multiway_cluster_sensitivity_rho0['benchmark'].equals( - dml_plr_multiway_cluster_sensitivity_rho0['benchmark_manual']) + assert all(dml_plr_multiway_cluster_sensitivity_rho0["benchmark"].columns == expected_columns) + assert all(dml_plr_multiway_cluster_sensitivity_rho0["benchmark"].index == ["d"]) + assert dml_plr_multiway_cluster_sensitivity_rho0["benchmark"].equals( + dml_plr_multiway_cluster_sensitivity_rho0["benchmark_manual"] + ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_plr_multiway_cluster_sensitivity_rho0_se(): n_folds = 3 cf_y = 0.03 @@ -109,18 +111,12 @@ def dml_plr_multiway_cluster_sensitivity_rho0_se(): ml_m = LinearRegression() np.random.seed(3141) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_cluster_data, - ml_l, ml_m, - n_folds=n_folds, - score='partialling out') + dml_plr_obj = dml.DoubleMLPLR(obj_dml_cluster_data, ml_l, ml_m, n_folds=n_folds, score="partialling out") dml_plr_obj.fit() - dml_plr_obj.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, - rho=0.0, level=level, null_hypothesis=0.0) + dml_plr_obj.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, rho=0.0, level=level, null_hypothesis=0.0) - res_dict = {'coef': dml_plr_obj.coef, - 'se': dml_plr_obj.se, - 'sensitivity_params': dml_plr_obj.sensitivity_params} + res_dict = {"coef": dml_plr_obj.coef, "se": dml_plr_obj.se, "sensitivity_params": dml_plr_obj.sensitivity_params} return res_dict @@ -128,9 +124,15 @@ def dml_plr_multiway_cluster_sensitivity_rho0_se(): # only valid for 'partialling out '; This might have slightly less precision in the calculations @pytest.mark.ci def test_dml_pliv_multiway_cluster_sensitivity_se(dml_plr_multiway_cluster_sensitivity_rho0_se): - assert math.isclose(dml_plr_multiway_cluster_sensitivity_rho0_se['se'][0], - dml_plr_multiway_cluster_sensitivity_rho0_se['sensitivity_params']['se']['lower'][0], - rel_tol=1e-9, abs_tol=1e-3) - assert math.isclose(dml_plr_multiway_cluster_sensitivity_rho0_se['se'][0], - dml_plr_multiway_cluster_sensitivity_rho0_se['sensitivity_params']['se']['upper'][0], - rel_tol=1e-9, abs_tol=1e-3) + assert math.isclose( + dml_plr_multiway_cluster_sensitivity_rho0_se["se"][0], + dml_plr_multiway_cluster_sensitivity_rho0_se["sensitivity_params"]["se"]["lower"][0], + rel_tol=1e-9, + abs_tol=1e-3, + ) + assert math.isclose( + dml_plr_multiway_cluster_sensitivity_rho0_se["se"][0], + dml_plr_multiway_cluster_sensitivity_rho0_se["sensitivity_params"]["se"]["upper"][0], + rel_tol=1e-9, + abs_tol=1e-3, + ) diff --git a/doubleml/tests/test_set_ml_nuisance_params.py b/doubleml/tests/test_set_ml_nuisance_params.py index ac2953b70..a189b184b 100644 --- a/doubleml/tests/test_set_ml_nuisance_params.py +++ b/doubleml/tests/test_set_ml_nuisance_params.py @@ -9,7 +9,7 @@ n_est_default = 100 n_est_test = 5 n_folds = 2 -test_values = [[{'n_estimators': 5}, {'n_estimators': 5}]] +test_values = [[{"n_estimators": 5}, {"n_estimators": 5}]] np.random.seed(3141) dml_data_plr = make_plr_CCDDHNR2018(n_obs=100) @@ -22,18 +22,16 @@ # linear models dml_plr = DoubleMLPLR(dml_data_plr, reg_learner, reg_learner, n_folds=n_folds) -dml_pliv = DoubleMLPLIV(dml_data_pliv, reg_learner, reg_learner, - reg_learner, n_folds=n_folds) +dml_pliv = DoubleMLPLIV(dml_data_pliv, reg_learner, reg_learner, reg_learner, n_folds=n_folds) dml_irm = DoubleMLIRM(dml_data_irm, reg_learner, class_learner, n_folds=n_folds) -dml_iivm = DoubleMLIIVM(dml_data_iivm, reg_learner, class_learner, - class_learner, n_folds=n_folds) +dml_iivm = DoubleMLIIVM(dml_data_iivm, reg_learner, class_learner, class_learner, n_folds=n_folds) dml_cvar = DoubleMLCVAR(dml_data_irm, ml_g=reg_learner, ml_m=class_learner, n_folds=n_folds) -dml_plr.set_ml_nuisance_params('ml_l', 'd', {'n_estimators': n_est_test}) -dml_pliv.set_ml_nuisance_params('ml_l', 'd', {'n_estimators': n_est_test}) -dml_irm.set_ml_nuisance_params('ml_g0', 'd', {'n_estimators': n_est_test}) -dml_iivm.set_ml_nuisance_params('ml_g0', 'd', {'n_estimators': n_est_test}) -dml_cvar.set_ml_nuisance_params('ml_g', 'd', {'n_estimators': n_est_test}) +dml_plr.set_ml_nuisance_params("ml_l", "d", {"n_estimators": n_est_test}) +dml_pliv.set_ml_nuisance_params("ml_l", "d", {"n_estimators": n_est_test}) +dml_irm.set_ml_nuisance_params("ml_g0", "d", {"n_estimators": n_est_test}) +dml_iivm.set_ml_nuisance_params("ml_g0", "d", {"n_estimators": n_est_test}) +dml_cvar.set_ml_nuisance_params("ml_g", "d", {"n_estimators": n_est_test}) dml_plr.fit(store_models=True) dml_pliv.fit(store_models=True) @@ -45,59 +43,59 @@ dml_pq = DoubleMLPQ(dml_data_irm, ml_g=class_learner, ml_m=class_learner, n_folds=n_folds) dml_lpq = DoubleMLLPQ(dml_data_iivm, ml_g=class_learner, ml_m=class_learner, n_folds=n_folds) -dml_pq.set_ml_nuisance_params('ml_g', 'd', {'n_estimators': n_est_test}) -dml_lpq.set_ml_nuisance_params('ml_m_z', 'd', {'n_estimators': n_est_test}) +dml_pq.set_ml_nuisance_params("ml_g", "d", {"n_estimators": n_est_test}) +dml_lpq.set_ml_nuisance_params("ml_m_z", "d", {"n_estimators": n_est_test}) dml_pq.fit(store_models=True) dml_lpq.fit(store_models=True) def _assert_nuisance_params(dml_obj, learner_1, learner_2): - assert dml_obj.params[learner_1]['d'] == test_values - assert dml_obj.params[learner_2]['d'][0] is None + assert dml_obj.params[learner_1]["d"] == test_values + assert dml_obj.params[learner_2]["d"][0] is None - param_list_1 = [dml_obj.models[learner_1]['d'][0][fold].n_estimators for fold in range(n_folds)] + param_list_1 = [dml_obj.models[learner_1]["d"][0][fold].n_estimators for fold in range(n_folds)] assert all(param == n_est_test for param in param_list_1) - param_list_2 = [dml_obj.models[learner_2]['d'][0][fold].n_estimators for fold in range(n_folds)] + param_list_2 = [dml_obj.models[learner_2]["d"][0][fold].n_estimators for fold in range(n_folds)] assert all(param == n_est_default for param in param_list_2) @pytest.mark.ci def test_plr_params(): - _assert_nuisance_params(dml_plr, 'ml_l', 'ml_m') + _assert_nuisance_params(dml_plr, "ml_l", "ml_m") @pytest.mark.ci def test_pliv_params(): - _assert_nuisance_params(dml_pliv, 'ml_l', 'ml_m') + _assert_nuisance_params(dml_pliv, "ml_l", "ml_m") @pytest.mark.ci def test_irm_params(): - _assert_nuisance_params(dml_irm, 'ml_g0', 'ml_g1') + _assert_nuisance_params(dml_irm, "ml_g0", "ml_g1") @pytest.mark.ci def test_iivm_params(): - _assert_nuisance_params(dml_iivm, 'ml_g0', 'ml_g1') + _assert_nuisance_params(dml_iivm, "ml_g0", "ml_g1") @pytest.mark.ci def test_cvar_params(): - _assert_nuisance_params(dml_cvar, 'ml_g', 'ml_m') + _assert_nuisance_params(dml_cvar, "ml_g", "ml_m") @pytest.mark.ci def test_pq_params(): - _assert_nuisance_params(dml_pq, 'ml_g', 'ml_m') + _assert_nuisance_params(dml_pq, "ml_g", "ml_m") @pytest.mark.ci def test_lpq_params(): - _assert_nuisance_params(dml_lpq, 'ml_m_z', 'ml_m_d_z0') - param_list_2 = [dml_lpq.models['ml_m_d_z1']['d'][0][fold].n_estimators for fold in range(n_folds)] + _assert_nuisance_params(dml_lpq, "ml_m_z", "ml_m_d_z0") + param_list_2 = [dml_lpq.models["ml_m_d_z1"]["d"][0][fold].n_estimators for fold in range(n_folds)] assert all(param == n_est_default for param in param_list_2) - param_list_2 = [dml_lpq.models['ml_g_du_z0']['d'][0][fold].n_estimators for fold in range(n_folds)] + param_list_2 = [dml_lpq.models["ml_g_du_z0"]["d"][0][fold].n_estimators for fold in range(n_folds)] assert all(param == n_est_default for param in param_list_2) - param_list_2 = [dml_lpq.models['ml_g_du_z1']['d'][0][fold].n_estimators for fold in range(n_folds)] + param_list_2 = [dml_lpq.models["ml_g_du_z1"]["d"][0][fold].n_estimators for fold in range(n_folds)] assert all(param == n_est_default for param in param_list_2) diff --git a/doubleml/tests/test_set_sample_splitting.py b/doubleml/tests/test_set_sample_splitting.py index 771198553..f9b9d5b5b 100644 --- a/doubleml/tests/test_set_sample_splitting.py +++ b/doubleml/tests/test_set_sample_splitting.py @@ -10,9 +10,7 @@ n_obs = dml_data.n_obs ml_l = Lasso() ml_m = Lasso() -dml_plr = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=7, n_rep=8, - draw_sample_splitting=False) +dml_plr = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=7, n_rep=8, draw_sample_splitting=False) def _assert_resampling_pars(dml_obj0, dml_obj1): @@ -41,12 +39,12 @@ def test_doubleml_set_sample_splitting_tuple(): _assert_smpls_equal([[smpls]], dml_plr.smpls) smpls = ([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]) - msg = 'Invalid partition provided. ' + 'Tuple provided that doesn\'t form a partition.' + msg = "Invalid partition provided. " + "Tuple provided that doesn't form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) smpls = ([0, 1, 2, 3, 4], [5, 6], [7, 8, 9]) - msg = 'Invalid partition provided. ' + 'Tuple for train_ind and test_ind must consist of exactly two elements.' + msg = "Invalid partition provided. " + "Tuple for train_ind and test_ind must consist of exactly two elements." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) @@ -54,29 +52,26 @@ def test_doubleml_set_sample_splitting_tuple(): @pytest.mark.ci def test_doubleml_set_sample_splitting_all_tuple(): # sample splitting with two folds and cross-fitting but no repeated cross-fitting - smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])] + smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])] dml_plr.set_sample_splitting(smpls) assert dml_plr.n_folds == 2 assert dml_plr.n_rep == 1 _assert_smpls_equal([smpls], dml_plr.smpls) - smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2], [3, 4])] - msg = 'Invalid partition provided. ' + 'All tuples for train_ind and test_ind must consist of exactly two elements.' + smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2], [3, 4])] + msg = "Invalid partition provided. " + "All tuples for train_ind and test_ind must consist of exactly two elements." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) # not valid partition smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9])] - msg = 'Invalid partition provided. ' + 'Tuples provided that don\'t form a partition.' + msg = "Invalid partition provided. " + "Tuples provided that don't form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) # sample splitting with cross-fitting and two folds that do not form a partition - smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8], [0, 1, 2, 3, 4, 9])] - msg = 'Invalid partition provided. ' + 'Tuples provided that don\'t form a partition.' + smpls = [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8], [0, 1, 2, 3, 4, 9])] + msg = "Invalid partition provided. " + "Tuples provided that don't form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) @@ -84,10 +79,10 @@ def test_doubleml_set_sample_splitting_all_tuple(): @pytest.mark.ci def test_doubleml_set_sample_splitting_all_list(): # sample splitting with two folds and repeated cross-fitting with n_rep = 2 - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] dml_plr.set_sample_splitting(smpls) assert dml_plr.n_folds == 2 assert dml_plr.n_rep == 2 @@ -100,54 +95,49 @@ def test_doubleml_set_sample_splitting_all_list(): dml_plr.set_sample_splitting(smpls) # second sample splitting is not a list - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - (([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8]))] - msg = ('Invalid partition provided. ' - 'all_smpls is a list where neither all elements are tuples nor all elements are lists.') + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + (([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])), + ] + msg = ( + "Invalid partition provided. " "all_smpls is a list where neither all elements are tuples nor all elements are lists." + ) with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [[[0, 2, 4, 6, 8], [1, 3, 5, 7, 9]], # not a tuple - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = 'For repeated sample splitting all_smpls must be list of lists of tuples.' + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [[[0, 2, 4, 6, 8], [1, 3, 5, 7, 9]], ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], # not a tuple + ] + msg = "For repeated sample splitting all_smpls must be list of lists of tuples." with pytest.raises(TypeError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4], [6, 8])]] - msg = 'Invalid partition provided. ' + 'All tuples for train_ind and test_ind must consist of exactly two elements.' + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4], [6, 8])], + ] + msg = "Invalid partition provided. " + "All tuples for train_ind and test_ind must consist of exactly two elements." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 6], [4, 5, 7, 8, 9]), - ([4, 5, 7, 8, 9], [0, 1, 2, 3, 6])], - [([0, 1, 4, 5, 7, 9], [2, 3, 6, 8]), - ([0, 2, 3, 4, 6, 8, 9], [1, 5, 7]), - ([1, 2, 3, 5, 6, 7, 8], [0, 4, 9])]] - msg = 'Invalid partition provided. ' + 'Different number of folds for repeated sample splitting.' + smpls = [ + [([0, 1, 2, 3, 6], [4, 5, 7, 8, 9]), ([4, 5, 7, 8, 9], [0, 1, 2, 3, 6])], + [([0, 1, 4, 5, 7, 9], [2, 3, 6, 8]), ([0, 2, 3, 4, 6, 8, 9], [1, 5, 7]), ([1, 2, 3, 5, 6, 7, 8], [0, 4, 9])], + ] + msg = "Invalid partition provided. " + "Different number of folds for repeated sample splitting." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) # sample splitting with cross-fitting and two folds that do not form a partition - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8], [0, 1, 2, 3, 4, 9])]] - msg = ('Invalid partition provided. ' - 'At least one inner list does not form a partition.') + smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8], [0, 1, 2, 3, 4, 9])]] + msg = "Invalid partition provided. " "At least one inner list does not form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) # repeated no-cross-fitting (does not form a partition) - smpls = [[([0, 1, 5, 7, 9], [2, 3, 4, 6, 8])], - [([2, 4, 7, 8, 9], [0, 1, 3, 5, 6])], - [([0, 1, 4, 6, 8], [2, 3, 5, 7, 9])]] - msg = ('Invalid partition provided. ' - 'At least one inner list does not form a partition.') + smpls = [[([0, 1, 5, 7, 9], [2, 3, 4, 6, 8])], [([2, 4, 7, 8, 9], [0, 1, 3, 5, 6])], [([0, 1, 4, 6, 8], [2, 3, 5, 7, 9])]] + msg = "Invalid partition provided. " "At least one inner list does not form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) @@ -157,42 +147,36 @@ def test_doubleml_draw_vs_set(): np.random.seed(3141) dml_plr_set = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=7, n_rep=8) - msg = 'n_folds must be greater than 1. You can use set_sample_splitting with a tuple to only use one fold.' + msg = "n_folds must be greater than 1. You can use set_sample_splitting with a tuple to only use one fold." with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=1, n_rep=1) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=1, n_rep=1) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=2, n_rep=1) + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=2, n_rep=1) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls[0]) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) - msg = 'Invalid partition provided. Tuple provided that doesn\'t form a partition.' + msg = "Invalid partition provided. Tuple provided that doesn't form a partition." with pytest.raises(ValueError, match=msg): dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls[0][0]) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=2, n_rep=1) + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=2, n_rep=1) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls[0]) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=5, n_rep=1) + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=5, n_rep=1) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls[0]) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=5, n_rep=3) + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=5, n_rep=3) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, - n_folds=2, n_rep=4) + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=2, n_rep=4) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) @@ -200,58 +184,58 @@ def test_doubleml_draw_vs_set(): @pytest.mark.ci def test_doubleml_set_sample_splitting_invalid_sets(): # sample splitting with two folds and repeated cross-fitting with n_rep = 2 - smpls = [[([0, 1.2, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = 'Invalid sample split. Train indices must be of type integer.' + smpls = [ + [([0, 1.2, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = "Invalid sample split. Train indices must be of type integer." with pytest.raises(TypeError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3.5, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = 'Invalid sample split. Test indices must be of type integer.' + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3.5, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = "Invalid sample split. Test indices must be of type integer." with pytest.raises(TypeError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 3, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = 'Invalid sample split. Intersection of train and test indices is not empty.' + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 3, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = "Invalid sample split. Intersection of train and test indices is not empty." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), - ([5, 6, 7, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = 'Invalid sample split. Train indices contain non-unique entries.' + smpls = [ + [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = "Invalid sample split. Train indices contain non-unique entries." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, 5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = 'Invalid sample split. Test indices contain non-unique entries.' + smpls = [ + [([0, 1, 2, 3, 4], [5, 5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = "Invalid sample split. Test indices contain non-unique entries." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 20], [5, 6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = r'Invalid sample split. Train indices must be in \[0, n_obs\).' + smpls = [ + [([0, 1, 2, 3, 20], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = r"Invalid sample split. Train indices must be in \[0, n_obs\)." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) - smpls = [[([0, 1, 2, 3, 4], [5, -6, 7, 8, 9]), - ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], - [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), - ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])]] - msg = r'Invalid sample split. Test indices must be in \[0, n_obs\).' + smpls = [ + [([0, 1, 2, 3, 4], [5, -6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], + [([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])], + ] + msg = r"Invalid sample split. Test indices must be in \[0, n_obs\)." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) From fd13e1d8ef059264649dbbc5757259c444f62f40 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:42:19 +0100 Subject: [PATCH 14/28] run ruff on utils --- doubleml/utils/__init__.py | 7 +++---- doubleml/utils/_checks.py | 6 +++--- doubleml/utils/_estimation.py | 13 +++++-------- doubleml/utils/blp.py | 8 ++++---- doubleml/utils/global_learner.py | 3 +-- doubleml/utils/policytree.py | 1 - doubleml/utils/resampling.py | 1 - doubleml/utils/tests/_utils_blp_manual.py | 2 +- doubleml/utils/tests/test_blp.py | 6 ++++-- doubleml/utils/tests/test_dummy_learners.py | 5 +++-- .../utils/tests/test_exceptions_gain_statistics.py | 2 +- .../utils/tests/test_exceptions_global_learners.py | 4 ++-- doubleml/utils/tests/test_global_learners.py | 7 ++++--- doubleml/utils/tests/test_policytree.py | 7 ++++--- .../utils/tests/test_var_est_and_aggregation.py | 4 ++-- 15 files changed, 37 insertions(+), 39 deletions(-) diff --git a/doubleml/utils/__init__.py b/doubleml/utils/__init__.py index 5281435ec..036d2008b 100644 --- a/doubleml/utils/__init__.py +++ b/doubleml/utils/__init__.py @@ -2,13 +2,12 @@ The :mod:`doubleml.utils` module includes various utilities. """ -from .dummy_learners import DMLDummyRegressor -from .dummy_learners import DMLDummyClassifier -from .resampling import DoubleMLResampling, DoubleMLClusterResampling from .blp import DoubleMLBLP -from .policytree import DoubleMLPolicyTree +from .dummy_learners import DMLDummyClassifier, DMLDummyRegressor from .gain_statistics import gain_statistics from .global_learner import GlobalClassifier, GlobalRegressor +from .policytree import DoubleMLPolicyTree +from .resampling import DoubleMLClusterResampling, DoubleMLResampling __all__ = [ "DMLDummyRegressor", diff --git a/doubleml/utils/_checks.py b/doubleml/utils/_checks.py index 8a546cb15..3be9bfe15 100644 --- a/doubleml/utils/_checks.py +++ b/doubleml/utils/_checks.py @@ -1,7 +1,7 @@ -import numpy as np -import warnings import inspect +import warnings +import numpy as np from sklearn.utils.multiclass import type_of_target @@ -496,7 +496,7 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d def _check_supports_sample_weights(learner, learner_name): - if not ('sample_weight' in inspect.signature(learner.fit).parameters): + if 'sample_weight' not in inspect.signature(learner.fit).parameters: raise ValueError(f"The {learner_name} learner {str(learner)} does not support sample weights. " "Please choose a learner that supports sample weights.") return diff --git a/doubleml/utils/_estimation.py b/doubleml/utils/_estimation.py index b88a75c7b..354f0f72d 100644 --- a/doubleml/utils/_estimation.py +++ b/doubleml/utils/_estimation.py @@ -1,17 +1,14 @@ -import numpy as np import warnings -from scipy.optimize import minimize_scalar -from sklearn.model_selection import cross_val_predict +import numpy as np +from joblib import Parallel, delayed +from scipy.optimize import minimize_scalar from sklearn.base import clone +from sklearn.metrics import log_loss, root_mean_squared_error +from sklearn.model_selection import GridSearchCV, KFold, RandomizedSearchCV, cross_val_predict from sklearn.preprocessing import LabelEncoder -from sklearn.model_selection import KFold, GridSearchCV, RandomizedSearchCV -from sklearn.metrics import root_mean_squared_error, log_loss - from statsmodels.nonparametric.kde import KDEUnivariate -from joblib import Parallel, delayed - from ._checks import _check_is_partition diff --git a/doubleml/utils/blp.py b/doubleml/utils/blp.py index 24bd807b7..159a74ed6 100644 --- a/doubleml/utils/blp.py +++ b/doubleml/utils/blp.py @@ -1,10 +1,10 @@ -import statsmodels.api as sm -import numpy as np -import pandas as pd import warnings -from scipy.stats import norm +import numpy as np +import pandas as pd +import statsmodels.api as sm from scipy.linalg import sqrtm +from scipy.stats import norm class DoubleMLBLP: diff --git a/doubleml/utils/global_learner.py b/doubleml/utils/global_learner.py index 1a38062a3..250f7efb3 100644 --- a/doubleml/utils/global_learner.py +++ b/doubleml/utils/global_learner.py @@ -1,5 +1,4 @@ -from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin, is_regressor, is_classifier, clone - +from sklearn.base import BaseEstimator, ClassifierMixin, RegressorMixin, clone, is_classifier, is_regressor from sklearn.utils.multiclass import unique_labels diff --git a/doubleml/utils/policytree.py b/doubleml/utils/policytree.py index f3aaa6321..59ee63ecc 100644 --- a/doubleml/utils/policytree.py +++ b/doubleml/utils/policytree.py @@ -1,6 +1,5 @@ import numpy as np import pandas as pd - from sklearn.tree import DecisionTreeClassifier, plot_tree from sklearn.utils.validation import check_is_fitted diff --git a/doubleml/utils/resampling.py b/doubleml/utils/resampling.py index 63aec0eb1..2d8b1e971 100644 --- a/doubleml/utils/resampling.py +++ b/doubleml/utils/resampling.py @@ -1,5 +1,4 @@ import numpy as np - from sklearn.model_selection import KFold, RepeatedKFold, RepeatedStratifiedKFold diff --git a/doubleml/utils/tests/_utils_blp_manual.py b/doubleml/utils/tests/_utils_blp_manual.py index c64545aa3..00985facd 100644 --- a/doubleml/utils/tests/_utils_blp_manual.py +++ b/doubleml/utils/tests/_utils_blp_manual.py @@ -1,8 +1,8 @@ import numpy as np +import pandas as pd import statsmodels.api as sm from scipy.linalg import sqrtm from scipy.stats import norm -import pandas as pd def fit_blp(orth_signal, basis, cov_type, **kwargs): diff --git a/doubleml/utils/tests/test_blp.py b/doubleml/utils/tests/test_blp.py index 38c1fff42..0a8e51025 100644 --- a/doubleml/utils/tests/test_blp.py +++ b/doubleml/utils/tests/test_blp.py @@ -1,10 +1,12 @@ +import copy + import numpy as np import pandas as pd import pytest -import copy import doubleml as dml -from ._utils_blp_manual import fit_blp, blp_confint + +from ._utils_blp_manual import blp_confint, fit_blp @pytest.fixture(scope='module', diff --git a/doubleml/utils/tests/test_dummy_learners.py b/doubleml/utils/tests/test_dummy_learners.py index c23088faa..846166ff7 100644 --- a/doubleml/utils/tests/test_dummy_learners.py +++ b/doubleml/utils/tests/test_dummy_learners.py @@ -1,8 +1,9 @@ -import pytest import numpy as np -from doubleml.utils import DMLDummyRegressor, DMLDummyClassifier +import pytest from sklearn.base import clone +from doubleml.utils import DMLDummyClassifier, DMLDummyRegressor + @pytest.fixture(scope="module") def dl_fixture(): diff --git a/doubleml/utils/tests/test_exceptions_gain_statistics.py b/doubleml/utils/tests/test_exceptions_gain_statistics.py index 734185eb4..a7bcc7f05 100644 --- a/doubleml/utils/tests/test_exceptions_gain_statistics.py +++ b/doubleml/utils/tests/test_exceptions_gain_statistics.py @@ -1,5 +1,5 @@ -import pytest import numpy as np +import pytest from doubleml.utils.gain_statistics import gain_statistics diff --git a/doubleml/utils/tests/test_exceptions_global_learners.py b/doubleml/utils/tests/test_exceptions_global_learners.py index ccd393222..0f601d70a 100644 --- a/doubleml/utils/tests/test_exceptions_global_learners.py +++ b/doubleml/utils/tests/test_exceptions_global_learners.py @@ -1,7 +1,7 @@ import pytest -from sklearn.linear_model import LogisticRegression, LinearRegression +from sklearn.linear_model import LinearRegression, LogisticRegression -from doubleml.utils import GlobalRegressor, GlobalClassifier +from doubleml.utils import GlobalClassifier, GlobalRegressor @pytest.mark.ci diff --git a/doubleml/utils/tests/test_global_learners.py b/doubleml/utils/tests/test_global_learners.py index 9cae0941d..ee3aedb94 100644 --- a/doubleml/utils/tests/test_global_learners.py +++ b/doubleml/utils/tests/test_global_learners.py @@ -1,9 +1,10 @@ -import pytest import numpy as np -from doubleml.utils import GlobalRegressor, GlobalClassifier +import pytest from sklearn.base import clone +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor from sklearn.linear_model import LinearRegression, LogisticRegression -from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier + +from doubleml.utils import GlobalClassifier, GlobalRegressor @pytest.fixture(scope='module', diff --git a/doubleml/utils/tests/test_policytree.py b/doubleml/utils/tests/test_policytree.py index 25f51a387..f07906d2f 100644 --- a/doubleml/utils/tests/test_policytree.py +++ b/doubleml/utils/tests/test_policytree.py @@ -1,13 +1,14 @@ +import copy + import numpy as np import pandas as pd import pytest -import copy +from sklearn.exceptions import NotFittedError +from sklearn.tree import DecisionTreeClassifier import doubleml as dml from ._utils_pt_manual import fit_policytree -from sklearn.tree import DecisionTreeClassifier -from sklearn.exceptions import NotFittedError @pytest.fixture(scope='module', diff --git a/doubleml/utils/tests/test_var_est_and_aggregation.py b/doubleml/utils/tests/test_var_est_and_aggregation.py index c209c513b..8273423df 100644 --- a/doubleml/utils/tests/test_var_est_and_aggregation.py +++ b/doubleml/utils/tests/test_var_est_and_aggregation.py @@ -1,7 +1,7 @@ -import pytest import numpy as np +import pytest -from doubleml.utils._estimation import _var_est, _aggregate_coefs_and_ses +from doubleml.utils._estimation import _aggregate_coefs_and_ses, _var_est @pytest.fixture(scope='module', From b49a52621f6b84f2aacb13b8b418b07b628f912a Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:43:07 +0100 Subject: [PATCH 15/28] format utils --- doubleml/utils/__init__.py | 2 +- doubleml/utils/_checks.py | 374 ++++++++++-------- doubleml/utils/_descriptive.py | 5 +- doubleml/utils/_estimation.py | 126 +++--- doubleml/utils/_plots.py | 131 +++--- doubleml/utils/blp.py | 101 +++-- doubleml/utils/gain_statistics.py | 86 ++-- doubleml/utils/global_learner.py | 6 +- doubleml/utils/policytree.py | 57 ++- doubleml/utils/resampling.py | 31 +- doubleml/utils/tests/_utils_blp_manual.py | 7 +- doubleml/utils/tests/_utils_pt_manual.py | 8 +- doubleml/utils/tests/test_blp.py | 120 +++--- .../tests/test_exceptions_gain_statistics.py | 137 +++---- doubleml/utils/tests/test_global_learners.py | 15 +- doubleml/utils/tests/test_policytree.py | 67 ++-- .../tests/test_var_est_and_aggregation.py | 54 +-- 17 files changed, 679 insertions(+), 648 deletions(-) diff --git a/doubleml/utils/__init__.py b/doubleml/utils/__init__.py index 036d2008b..386586ce9 100644 --- a/doubleml/utils/__init__.py +++ b/doubleml/utils/__init__.py @@ -18,5 +18,5 @@ "DoubleMLPolicyTree", "gain_statistics", "GlobalClassifier", - "GlobalRegressor" + "GlobalRegressor", ] diff --git a/doubleml/utils/_checks.py b/doubleml/utils/_checks.py index 3be9bfe15..65c9492f6 100644 --- a/doubleml/utils/_checks.py +++ b/doubleml/utils/_checks.py @@ -7,60 +7,48 @@ def _check_in_zero_one(value, name, include_zero=True, include_one=True): if not isinstance(value, float): - raise TypeError(f'{name} must be of float type. ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError(f"{name} must be of float type. " f"{str(value)} of type {str(type(value))} was passed.") if include_zero & include_one: if (value < 0) | (value > 1): - raise ValueError(f'{name} must be in [0,1]. ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be in [0,1]. " f"{str(value)} was passed.") elif (not include_zero) & include_one: if (value <= 0) | (value > 1): - raise ValueError(f'{name} must be in (0,1]. ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be in (0,1]. " f"{str(value)} was passed.") elif include_zero & (not include_one): if (value < 0) | (value >= 1): - raise ValueError(f'{name} must be in [0,1). ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be in [0,1). " f"{str(value)} was passed.") else: if (value <= 0) | (value >= 1): - raise ValueError(f'{name} must be in (0,1). ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be in (0,1). " f"{str(value)} was passed.") return def _check_integer(value, name, lower_bound=None, upper_bound=None): if not isinstance(value, int): - raise TypeError(f'{name} must be an integer.' - f' {str(value)} of type {str(type(value))} was passed.') + raise TypeError(f"{name} must be an integer." f" {str(value)} of type {str(type(value))} was passed.") if lower_bound is not None: if value < lower_bound: - raise ValueError(f'{name} must be larger or equal to {lower_bound}. ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be larger or equal to {lower_bound}. " f"{str(value)} was passed.") if upper_bound is not None: if value > upper_bound: - raise ValueError(f'{name} must be smaller or equal to {upper_bound}. ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be smaller or equal to {upper_bound}. " f"{str(value)} was passed.") return def _check_float(value, name, lower_bound=None, upper_bound=None): if not isinstance(value, float): - raise TypeError(f'{name} must be of float type.' - f' {str(value)} of type {str(type(value))} was passed.') + raise TypeError(f"{name} must be of float type." f" {str(value)} of type {str(type(value))} was passed.") if lower_bound is not None: if value < lower_bound: - raise ValueError(f'{name} must be larger or equal to {lower_bound}. ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be larger or equal to {lower_bound}. " f"{str(value)} was passed.") if upper_bound is not None: if value > upper_bound: - raise ValueError(f'{name} must be smaller or equal to {upper_bound}. ' - f'{str(value)} was passed.') + raise ValueError(f"{name} must be smaller or equal to {upper_bound}. " f"{str(value)} was passed.") def _check_bool(value, name): if not isinstance(value, bool): - raise TypeError(f'{name} has to be boolean.' - f' {str(value)} of type {str(type(value))} was passed.') + raise TypeError(f"{name} has to be boolean." f" {str(value)} of type {str(type(value))} was passed.") def _check_is_partition(smpls, n_obs): @@ -93,18 +81,18 @@ def _check_smpl_split_tpl(tpl, n_obs, check_intersect=False): test_index = np.sort(np.array(tpl[1])) if not issubclass(train_index.dtype.type, np.integer): - raise TypeError('Invalid sample split. Train indices must be of type integer.') + raise TypeError("Invalid sample split. Train indices must be of type integer.") if not issubclass(test_index.dtype.type, np.integer): - raise TypeError('Invalid sample split. Test indices must be of type integer.') + raise TypeError("Invalid sample split. Test indices must be of type integer.") if check_intersect: if set(train_index) & set(test_index): - raise ValueError('Invalid sample split. Intersection of train and test indices is not empty.') + raise ValueError("Invalid sample split. Intersection of train and test indices is not empty.") if len(np.unique(train_index)) != len(train_index): - raise ValueError('Invalid sample split. Train indices contain non-unique entries.') + raise ValueError("Invalid sample split. Train indices contain non-unique entries.") if len(np.unique(test_index)) != len(test_index): - raise ValueError('Invalid sample split. Test indices contain non-unique entries.') + raise ValueError("Invalid sample split. Test indices contain non-unique entries.") # we sort the indices above # if not np.all(np.diff(train_index) > 0): @@ -113,9 +101,9 @@ def _check_smpl_split_tpl(tpl, n_obs, check_intersect=False): # raise NotImplementedError('Invalid sample split. Only sorted test indices are supported.') if not set(train_index).issubset(range(n_obs)): - raise ValueError('Invalid sample split. Train indices must be in [0, n_obs).') + raise ValueError("Invalid sample split. Train indices must be in [0, n_obs).") if not set(test_index).issubset(range(n_obs)): - raise ValueError('Invalid sample split. Test indices must be in [0, n_obs).') + raise ValueError("Invalid sample split. Test indices must be in [0, n_obs).") return train_index, test_index @@ -123,119 +111,127 @@ def _check_smpl_split_tpl(tpl, n_obs, check_intersect=False): def _check_finite_predictions(preds, learner, learner_name, smpls): test_indices = np.concatenate([test_index for _, test_index in smpls]) if not np.all(np.isfinite(preds[test_indices])): - raise ValueError(f'Predictions from learner {str(learner)} for {learner_name} are not finite.') + raise ValueError(f"Predictions from learner {str(learner)} for {learner_name} are not finite.") return def _check_score(score, valid_score, allow_callable=True): if isinstance(score, str): if score not in valid_score: - raise ValueError('Invalid score ' + score + '. ' + - 'Valid score ' + ' or '.join(valid_score) + '.') + raise ValueError("Invalid score " + score + ". " + "Valid score " + " or ".join(valid_score) + ".") else: if allow_callable: if not callable(score): - raise TypeError('score should be either a string or a callable. ' - f'{str(score)} was passed.') + raise TypeError("score should be either a string or a callable. " f"{str(score)} was passed.") else: - raise TypeError('score should be a string. ' - f'{str(score)} was passed.') + raise TypeError("score should be a string. " f"{str(score)} was passed.") return def _check_trimming(trimming_rule, trimming_threshold): - valid_trimming_rule = ['truncate'] + valid_trimming_rule = ["truncate"] if trimming_rule not in valid_trimming_rule: - raise ValueError('Invalid trimming_rule ' + str(trimming_rule) + '. ' + - 'Valid trimming_rule ' + ' or '.join(valid_trimming_rule) + '.') + raise ValueError( + "Invalid trimming_rule " + + str(trimming_rule) + + ". " + + "Valid trimming_rule " + + " or ".join(valid_trimming_rule) + + "." + ) if not isinstance(trimming_threshold, float): - raise TypeError('trimming_threshold has to be a float. ' + - f'Object of type {str(type(trimming_threshold))} passed.') + raise TypeError("trimming_threshold has to be a float. " + f"Object of type {str(type(trimming_threshold))} passed.") if (trimming_threshold <= 0) | (trimming_threshold >= 0.5): - raise ValueError('Invalid trimming_threshold ' + str(trimming_threshold) + '. ' + - 'trimming_threshold has to be between 0 and 0.5.') + raise ValueError( + "Invalid trimming_threshold " + str(trimming_threshold) + ". " + "trimming_threshold has to be between 0 and 0.5." + ) return def _check_zero_one_treatment(obj_dml): - one_treat = (obj_dml._dml_data.n_treat == 1) - binary_treat = (type_of_target(obj_dml._dml_data.d) == 'binary') + one_treat = obj_dml._dml_data.n_treat == 1 + binary_treat = type_of_target(obj_dml._dml_data.d) == "binary" zero_one_treat = np.all((np.power(obj_dml._dml_data.d, 2) - obj_dml._dml_data.d) == 0) if not (one_treat & binary_treat & zero_one_treat): - raise ValueError('Incompatible data. ' - f'To fit an {str(obj_dml.score)} model with DML ' - 'exactly one binary variable with values 0 and 1 ' - 'needs to be specified as treatment variable.') + raise ValueError( + "Incompatible data. " + f"To fit an {str(obj_dml.score)} model with DML " + "exactly one binary variable with values 0 and 1 " + "needs to be specified as treatment variable." + ) def _check_treatment(treatment): if not isinstance(treatment, int): - raise TypeError('Treatment indicator has to be an integer. ' + - f'Object of type {str(type(treatment))} passed.') + raise TypeError("Treatment indicator has to be an integer. " + f"Object of type {str(type(treatment))} passed.") if (treatment != 0) & (treatment != 1): - raise ValueError('Treatment indicator has be either 0 or 1. ' + - f'Treatment indicator {str(treatment)} passed.') + raise ValueError("Treatment indicator has be either 0 or 1. " + f"Treatment indicator {str(treatment)} passed.") return def _check_quantile(quantile): if not isinstance(quantile, float): - raise TypeError('Quantile has to be a float. ' + - f'Object of type {str(type(quantile))} passed.') + raise TypeError("Quantile has to be a float. " + f"Object of type {str(type(quantile))} passed.") if (quantile <= 0) | (quantile >= 1): - raise ValueError('Quantile has be between 0 or 1. ' + - f'Quantile {str(quantile)} passed.') + raise ValueError("Quantile has be between 0 or 1. " + f"Quantile {str(quantile)} passed.") return def _check_contains_iv(obj_dml_data): if obj_dml_data.z_cols is not None: - raise ValueError('Incompatible data. ' + - ' and '.join(obj_dml_data.z_cols) + - ' have been set as instrumental variable(s). ' - 'To fit an local model see the documentation.') + raise ValueError( + "Incompatible data. " + " and ".join(obj_dml_data.z_cols) + " have been set as instrumental variable(s). " + "To fit an local model see the documentation." + ) return def _check_is_propensity(preds, learner, learner_name, smpls, eps=1e-12): test_indices = np.concatenate([test_index for _, test_index in smpls]) if any((preds[test_indices] < eps) | (preds[test_indices] > 1 - eps)): - warnings.warn(f'Propensity predictions from learner {str(learner)} for' - f' {learner_name} are close to zero or one (eps={eps}).') + warnings.warn( + f"Propensity predictions from learner {str(learner)} for" f" {learner_name} are close to zero or one (eps={eps})." + ) return def _check_binary_predictions(pred, learner, learner_name, variable_name): - binary_preds = (type_of_target(pred) == 'binary') + binary_preds = type_of_target(pred) == "binary" zero_one_preds = np.all((np.power(pred, 2) - pred) == 0) if binary_preds & zero_one_preds: - raise ValueError(f'For the binary variable {variable_name}, ' - f'predictions obtained with the {learner_name} learner {str(learner)} are also ' - 'observed to be binary with values 0 and 1. Make sure that for classifiers ' - 'probabilities and not labels are predicted.') + raise ValueError( + f"For the binary variable {variable_name}, " + f"predictions obtained with the {learner_name} learner {str(learner)} are also " + "observed to be binary with values 0 and 1. Make sure that for classifiers " + "probabilities and not labels are predicted." + ) def _check_benchmarks(benchmarks): if benchmarks is not None: if not isinstance(benchmarks, dict): - raise TypeError('benchmarks has to be either None or a dictionary. ' - f'{str(benchmarks)} of type {type(benchmarks)} was passed.') - if not set(benchmarks.keys()) == {'cf_y', 'cf_d', 'name'}: - raise ValueError('benchmarks has to be a dictionary with keys cf_y, cf_d and name. ' - f'Got {str(benchmarks.keys())}.') + raise TypeError( + "benchmarks has to be either None or a dictionary. " + f"{str(benchmarks)} of type {type(benchmarks)} was passed." + ) + if not set(benchmarks.keys()) == {"cf_y", "cf_d", "name"}: + raise ValueError( + "benchmarks has to be a dictionary with keys cf_y, cf_d and name. " f"Got {str(benchmarks.keys())}." + ) value_lengths = [len(value) for value in benchmarks.values()] if not len(set(value_lengths)) == 1: - raise ValueError('benchmarks has to be a dictionary with values of same length. ' - f'Got {str(value_lengths)}.') - for i in (range(value_lengths[0])): - for key in ['cf_y', 'cf_d']: + raise ValueError("benchmarks has to be a dictionary with values of same length. " f"Got {str(value_lengths)}.") + for i in range(value_lengths[0]): + for key in ["cf_y", "cf_d"]: _check_in_zero_one(benchmarks[key][i], f"benchmarks {key}", include_zero=True, include_one=False) if not isinstance(benchmarks["name"][i], str): - raise TypeError('benchmarks name must be of string type. ' - f'{str(benchmarks["name"][i])} of type {str(type(benchmarks["name"][i]))} was passed.') + raise TypeError( + "benchmarks name must be of string type. " + f'{str(benchmarks["name"][i])} of type {str(type(benchmarks["name"][i]))} was passed.' + ) return @@ -244,14 +240,14 @@ def _check_weights(weights, score, n_obs, n_rep): # check general type if (not isinstance(weights, np.ndarray)) and (not isinstance(weights, dict)): - raise TypeError("weights must be a numpy array or dictionary. " - f"weights of type {str(type(weights))} was passed.") + raise TypeError( + "weights must be a numpy array or dictionary. " f"weights of type {str(type(weights))} was passed." + ) # check shape if isinstance(weights, np.ndarray): if (weights.ndim != 1) or weights.shape[0] != n_obs: - raise ValueError(f"weights must have shape ({n_obs},). " - f"weights of shape {weights.shape} was passed.") + raise ValueError(f"weights must have shape ({n_obs},). " f"weights of shape {weights.shape} was passed.") if not np.all(0 <= weights): raise ValueError("All weights values must be greater or equal 0.") if weights.sum() == 0: @@ -260,8 +256,9 @@ def _check_weights(weights, score, n_obs, n_rep): # check special form for ATTE score if score == "ATTE": if not isinstance(weights, np.ndarray): - raise TypeError("weights must be a numpy array for ATTE score. " - f"weights of type {str(type(weights))} was passed.") + raise TypeError( + "weights must be a numpy array for ATTE score. " f"weights of type {str(type(weights))} was passed." + ) is_binary = np.all((np.power(weights, 2) - weights) == 0) if not is_binary: @@ -272,16 +269,19 @@ def _check_weights(weights, score, n_obs, n_rep): assert score == "ATE" expected_keys = ["weights", "weights_bar"] if not set(weights.keys()) == set(expected_keys): - raise ValueError(f"weights must have keys {expected_keys}. " - f"keys {str(weights.keys())} were passed.") + raise ValueError(f"weights must have keys {expected_keys}. " f"keys {str(weights.keys())} were passed.") expected_shapes = [(n_obs,), (n_obs, n_rep)] if weights["weights"].shape != expected_shapes[0]: - raise ValueError(f"weights must have shape {expected_shapes[0]}. " - f"weights of shape {weights['weights'].shape} was passed.") + raise ValueError( + f"weights must have shape {expected_shapes[0]}. " + f"weights of shape {weights['weights'].shape} was passed." + ) if weights["weights_bar"].shape != expected_shapes[1]: - raise ValueError(f"weights_bar must have shape {expected_shapes[1]}. " - f"weights_bar of shape {weights['weights_bar'].shape} was passed.") + raise ValueError( + f"weights_bar must have shape {expected_shapes[1]}. " + f"weights_bar of shape {weights['weights_bar'].shape} was passed." + ) if (not np.all(weights["weights"] >= 0)) or (not np.all(weights["weights_bar"] >= 0)): raise ValueError("All weights values must be greater or equal 0.") if (weights["weights"].sum() == 0) or (weights["weights_bar"].sum() == 0): @@ -292,76 +292,102 @@ def _check_weights(weights, score, n_obs, n_rep): def _check_external_predictions(external_predictions, valid_treatments, valid_learners, n_obs, n_rep): if external_predictions is not None: if not isinstance(external_predictions, dict): - raise TypeError('external_predictions must be a dictionary. ' - f'{str(external_predictions)} of type {str(type(external_predictions))} was passed.') + raise TypeError( + "external_predictions must be a dictionary. " + f"{str(external_predictions)} of type {str(type(external_predictions))} was passed." + ) supplied_treatments = list(external_predictions.keys()) if not set(supplied_treatments).issubset(valid_treatments): - raise ValueError('Invalid external_predictions. ' - f'Invalid treatment variable in {str(supplied_treatments)}. ' - 'Valid treatment variables ' + ' or '.join(valid_treatments) + '.') + raise ValueError( + "Invalid external_predictions. " + f"Invalid treatment variable in {str(supplied_treatments)}. " + "Valid treatment variables " + " or ".join(valid_treatments) + "." + ) for treatment in supplied_treatments: if not isinstance(external_predictions[treatment], dict): - raise TypeError('external_predictions must be a nested dictionary. ' - f'For treatment {str(treatment)} a value of type ' - f'{str(type(external_predictions[treatment]))} was passed.') + raise TypeError( + "external_predictions must be a nested dictionary. " + f"For treatment {str(treatment)} a value of type " + f"{str(type(external_predictions[treatment]))} was passed." + ) supplied_learners = list(external_predictions[treatment].keys()) if not set(supplied_learners).issubset(valid_learners): - raise ValueError('Invalid external_predictions. ' - f'Invalid nuisance learner for treatment {str(treatment)} in {str(supplied_learners)}. ' - 'Valid nuisance learners ' + ' or '.join(valid_learners) + '.') + raise ValueError( + "Invalid external_predictions. " + f"Invalid nuisance learner for treatment {str(treatment)} in {str(supplied_learners)}. " + "Valid nuisance learners " + " or ".join(valid_learners) + "." + ) for learner in supplied_learners: if not isinstance(external_predictions[treatment][learner], np.ndarray): - raise TypeError('Invalid external_predictions. ' - 'The values of the nested list must be a numpy array. ' - 'Invalid predictions for treatment ' + str(treatment) + - ' and learner ' + str(learner) + '. ' + - f'Object of type {str(type(external_predictions[treatment][learner]))} was passed.') + raise TypeError( + "Invalid external_predictions. " + "The values of the nested list must be a numpy array. " + "Invalid predictions for treatment " + + str(treatment) + + " and learner " + + str(learner) + + ". " + + f"Object of type {str(type(external_predictions[treatment][learner]))} was passed." + ) expected_shape = (n_obs, n_rep) if external_predictions[treatment][learner].shape != expected_shape: - raise ValueError('Invalid external_predictions. ' - f'The supplied predictions have to be of shape {str(expected_shape)}. ' - 'Invalid predictions for treatment ' + str(treatment) + - ' and learner ' + str(learner) + '. ' + - f'Predictions of shape {str(external_predictions[treatment][learner].shape)} passed.') + raise ValueError( + "Invalid external_predictions. " + f"The supplied predictions have to be of shape {str(expected_shape)}. " + "Invalid predictions for treatment " + + str(treatment) + + " and learner " + + str(learner) + + ". " + + f"Predictions of shape {str(external_predictions[treatment][learner].shape)} passed." + ) def _check_bootstrap(method, n_rep_boot): - if (not isinstance(method, str)) | (method not in ['Bayes', 'normal', 'wild']): - raise ValueError('Method must be "Bayes", "normal" or "wild". ' - f'Got {str(method)}.') + if (not isinstance(method, str)) | (method not in ["Bayes", "normal", "wild"]): + raise ValueError('Method must be "Bayes", "normal" or "wild". ' f"Got {str(method)}.") if not isinstance(n_rep_boot, int): - raise TypeError('The number of bootstrap replications must be of int type. ' - f'{str(n_rep_boot)} of type {str(type(n_rep_boot))} was passed.') + raise TypeError( + "The number of bootstrap replications must be of int type. " + f"{str(n_rep_boot)} of type {str(type(n_rep_boot))} was passed." + ) if n_rep_boot < 1: - raise ValueError('The number of bootstrap replications must be positive. ' - f'{str(n_rep_boot)} was passed.') + raise ValueError("The number of bootstrap replications must be positive. " f"{str(n_rep_boot)} was passed.") return def _check_framework_compatibility(dml_framework_1, dml_framework_2, check_treatments=True): if not dml_framework_1.n_obs == dml_framework_2.n_obs: - raise ValueError('The number of observations in DoubleMLFrameworks must be the same. ' - f'Got {str(dml_framework_1.n_obs)} and {str(dml_framework_2.n_obs)}.') + raise ValueError( + "The number of observations in DoubleMLFrameworks must be the same. " + f"Got {str(dml_framework_1.n_obs)} and {str(dml_framework_2.n_obs)}." + ) if not dml_framework_1.n_rep == dml_framework_2.n_rep: - raise ValueError('The number of replications in DoubleMLFrameworks must be the same. ' - f'Got {str(dml_framework_1.n_rep)} and {str(dml_framework_2.n_rep)}.') + raise ValueError( + "The number of replications in DoubleMLFrameworks must be the same. " + f"Got {str(dml_framework_1.n_rep)} and {str(dml_framework_2.n_rep)}." + ) if check_treatments: if not dml_framework_1.n_thetas == dml_framework_2.n_thetas: - raise ValueError('The number of parameters theta in DoubleMLFrameworks must be the same. ' - f'Got {str(dml_framework_1.n_thetas)} and {str(dml_framework_2.n_thetas)}.') + raise ValueError( + "The number of parameters theta in DoubleMLFrameworks must be the same. " + f"Got {str(dml_framework_1.n_thetas)} and {str(dml_framework_2.n_thetas)}." + ) if dml_framework_1._is_cluster_data != dml_framework_2._is_cluster_data: - raise ValueError('The cluster structure in DoubleMLFrameworks must be the same. ' - f'Got {str(dml_framework_1._is_cluster_data)} and {str(dml_framework_2._is_cluster_data)}.') + raise ValueError( + "The cluster structure in DoubleMLFrameworks must be the same. " + f"Got {str(dml_framework_1._is_cluster_data)} and {str(dml_framework_2._is_cluster_data)}." + ) return @@ -371,18 +397,17 @@ def _check_set(x): def _check_resampling_specification(n_folds, n_rep): if not isinstance(n_folds, int): - raise TypeError('The number of folds must be of int type. ' - f'{str(n_folds)} of type {str(type(n_folds))} was passed.') + raise TypeError("The number of folds must be of int type. " f"{str(n_folds)} of type {str(type(n_folds))} was passed.") if n_folds < 2: - raise ValueError('The number of folds greater or equal to 2. ' - f'{str(n_folds)} was passed.') + raise ValueError("The number of folds greater or equal to 2. " f"{str(n_folds)} was passed.") if not isinstance(n_rep, int): - raise TypeError('The number of repetitions for the sample splitting must be of int type. ' - f'{str(n_rep)} of type {str(type(n_rep))} was passed.') + raise TypeError( + "The number of repetitions for the sample splitting must be of int type. " + f"{str(n_rep)} of type {str(type(n_rep))} was passed." + ) if n_rep < 1: - raise ValueError('The number of repetitions for the sample splitting has to be positive. ' - f'{str(n_rep)} was passed.') + raise ValueError("The number of repetitions for the sample splitting has to be positive. " f"{str(n_rep)} was passed.") return @@ -397,26 +422,27 @@ def _check_cluster_partitions(smpls, values): def _check_cluster_sample_splitting(all_smpls_cluster, dml_data, n_rep, n_folds): if all_smpls_cluster is None: - raise ValueError('For cluster data, all_smpls_cluster must be provided.') + raise ValueError("For cluster data, all_smpls_cluster must be provided.") n_rep_cluster = len(all_smpls_cluster) if n_rep_cluster != n_rep: - raise ValueError('Invalid samples provided. ' - 'Number of repetitions for all_smpls and all_smpls_cluster must be the same.') + raise ValueError( + "Invalid samples provided. " "Number of repetitions for all_smpls and all_smpls_cluster must be the same." + ) for i_rep in range(n_rep): n_folds_cluster = len(all_smpls_cluster[i_rep]) if n_folds_cluster != n_folds: - raise ValueError('Invalid samples provided. ' - 'Number of folds for all_smpls and all_smpls_cluster must be the same.') + raise ValueError( + "Invalid samples provided. " "Number of folds for all_smpls and all_smpls_cluster must be the same." + ) for i_cluster in range(dml_data.n_cluster_vars): this_cluster_var = dml_data.cluster_vars[:, i_cluster] clusters = np.unique(this_cluster_var) cluster_partition = [all_smpls_cluster[0][0][0][i_cluster], all_smpls_cluster[0][0][1][i_cluster]] is_cluster_partition = _check_cluster_partitions(cluster_partition, clusters) if not is_cluster_partition: - raise ValueError('Invalid cluster partition provided. ' - 'At least one inner list does not form a partition.') + raise ValueError("Invalid cluster partition provided. " "At least one inner list does not form a partition.") smpls_cluster = all_smpls_cluster return smpls_cluster @@ -426,56 +452,61 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d if isinstance(all_smpls, tuple): if not len(all_smpls) == 2: - raise ValueError('Invalid partition provided. ' - 'Tuple for train_ind and test_ind must consist of exactly two elements.') + raise ValueError( + "Invalid partition provided. " "Tuple for train_ind and test_ind must consist of exactly two elements." + ) all_smpls = _check_smpl_split_tpl(all_smpls, dml_data.n_obs) - if (_check_is_partition([all_smpls], dml_data.n_obs) & - _check_is_partition([(all_smpls[1], all_smpls[0])], dml_data.n_obs)): + if _check_is_partition([all_smpls], dml_data.n_obs) & _check_is_partition( + [(all_smpls[1], all_smpls[0])], dml_data.n_obs + ): n_rep = 1 n_folds = 1 smpls = [[all_smpls]] else: - raise ValueError('Invalid partition provided. ' - 'Tuple provided that doesn\'t form a partition.') + raise ValueError("Invalid partition provided. " "Tuple provided that doesn't form a partition.") else: if not isinstance(all_smpls, list): - raise TypeError('all_smpls must be of list or tuple type. ' - f'{str(all_smpls)} of type {str(type(all_smpls))} was passed.') + raise TypeError( + "all_smpls must be of list or tuple type. " f"{str(all_smpls)} of type {str(type(all_smpls))} was passed." + ) all_tuple = all([isinstance(tpl, tuple) for tpl in all_smpls]) if all_tuple: if not all([len(tpl) == 2 for tpl in all_smpls]): - raise ValueError('Invalid partition provided. ' - 'All tuples for train_ind and test_ind must consist of exactly two elements.') + raise ValueError( + "Invalid partition provided. " + "All tuples for train_ind and test_ind must consist of exactly two elements." + ) n_rep = 1 all_smpls = _check_smpl_split(all_smpls, dml_data.n_obs) if _check_is_partition(all_smpls, dml_data.n_obs): - if ((len(all_smpls) == 1) & - _check_is_partition([(all_smpls[0][1], all_smpls[0][0])], dml_data.n_obs)): + if (len(all_smpls) == 1) & _check_is_partition([(all_smpls[0][1], all_smpls[0][0])], dml_data.n_obs): n_folds = 1 smpls = [all_smpls] else: n_folds = len(all_smpls) smpls = _check_all_smpls([all_smpls], dml_data.n_obs, check_intersect=True) else: - raise ValueError('Invalid partition provided. ' - 'Tuples provided that don\'t form a partition.') + raise ValueError("Invalid partition provided. " "Tuples provided that don't form a partition.") else: all_list = all([isinstance(smpl, list) for smpl in all_smpls]) if not all_list: - raise ValueError('Invalid partition provided. ' - 'all_smpls is a list where neither all elements are tuples ' - 'nor all elements are lists.') + raise ValueError( + "Invalid partition provided. " + "all_smpls is a list where neither all elements are tuples " + "nor all elements are lists." + ) all_tuple = all([all([isinstance(tpl, tuple) for tpl in smpl]) for smpl in all_smpls]) if not all_tuple: - raise TypeError('For repeated sample splitting all_smpls must be list of lists of tuples.') + raise TypeError("For repeated sample splitting all_smpls must be list of lists of tuples.") all_pairs = all([all([len(tpl) == 2 for tpl in smpl]) for smpl in all_smpls]) if not all_pairs: - raise ValueError('Invalid partition provided. ' - 'All tuples for train_ind and test_ind must consist of exactly two elements.') + raise ValueError( + "Invalid partition provided. " + "All tuples for train_ind and test_ind must consist of exactly two elements." + ) n_folds_each_smpl = np.array([len(smpl) for smpl in all_smpls]) if not np.all(n_folds_each_smpl == n_folds_each_smpl[0]): - raise ValueError('Invalid partition provided. ' - 'Different number of folds for repeated sample splitting.') + raise ValueError("Invalid partition provided. " "Different number of folds for repeated sample splitting.") all_smpls = _check_all_smpls(all_smpls, dml_data.n_obs) smpls_are_partitions = [_check_is_partition(smpl, dml_data.n_obs) for smpl in all_smpls] @@ -484,8 +515,7 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d n_folds = int(n_folds_each_smpl[0]) smpls = _check_all_smpls(all_smpls, dml_data.n_obs, check_intersect=True) else: - raise ValueError('Invalid partition provided. ' - 'At least one inner list does not form a partition.') + raise ValueError("Invalid partition provided. " "At least one inner list does not form a partition.") if is_cluster_data: smpls_cluster = _check_cluster_sample_splitting(all_smpls_cluster, dml_data, n_rep, n_folds) @@ -496,7 +526,9 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d def _check_supports_sample_weights(learner, learner_name): - if 'sample_weight' not in inspect.signature(learner.fit).parameters: - raise ValueError(f"The {learner_name} learner {str(learner)} does not support sample weights. " - "Please choose a learner that supports sample weights.") + if "sample_weight" not in inspect.signature(learner.fit).parameters: + raise ValueError( + f"The {learner_name} learner {str(learner)} does not support sample weights. " + "Please choose a learner that supports sample weights." + ) return diff --git a/doubleml/utils/_descriptive.py b/doubleml/utils/_descriptive.py index 54144bc8c..6868da677 100644 --- a/doubleml/utils/_descriptive.py +++ b/doubleml/utils/_descriptive.py @@ -3,9 +3,8 @@ def generate_summary(coef, se, t_stat, pval, ci, index_names=None): - col_names = ['coef', 'std err', 't', 'P>|t|'] - summary_stats = np.transpose(np.vstack( - [coef, se, t_stat, pval])) + col_names = ["coef", "std err", "t", "P>|t|"] + summary_stats = np.transpose(np.vstack([coef, se, t_stat, pval])) df_summary = pd.DataFrame(summary_stats, columns=col_names) if index_names is not None: df_summary.index = index_names diff --git a/doubleml/utils/_estimation.py b/doubleml/utils/_estimation.py index 354f0f72d..c33317777 100644 --- a/doubleml/utils/_estimation.py +++ b/doubleml/utils/_estimation.py @@ -16,7 +16,7 @@ def _assure_2d_array(x): if x.ndim == 1: x = x.reshape(-1, 1) elif x.ndim > 2: - raise ValueError('Only one- or two-dimensional arrays are allowed') + raise ValueError("Only one- or two-dimensional arrays are allowed") return x @@ -43,17 +43,19 @@ def _fit(estimator, x, y, train_index, idx=None): return estimator, idx -def _dml_cv_predict(estimator, x, y, smpls=None, - n_jobs=None, est_params=None, method='predict', return_train_preds=False, return_models=False): +def _dml_cv_predict( + estimator, x, y, smpls=None, n_jobs=None, est_params=None, method="predict", return_train_preds=False, return_models=False +): n_obs = x.shape[0] smpls_is_partition = _check_is_partition(smpls, n_obs) fold_specific_params = (est_params is not None) & (not isinstance(est_params, dict)) fold_specific_target = isinstance(y, list) - manual_cv_predict = (not smpls_is_partition) | return_train_preds | fold_specific_params | fold_specific_target \ - | return_models + manual_cv_predict = ( + (not smpls_is_partition) | return_train_preds | fold_specific_params | fold_specific_target | return_models + ) - res = {'models': None} + res = {"models": None} if not manual_cv_predict: if est_params is None: # if there are no parameters set we redirect to the standard method @@ -62,25 +64,24 @@ def _dml_cv_predict(estimator, x, y, smpls=None, assert isinstance(est_params, dict) # if no fold-specific parameters we redirect to the standard method # warnings.warn("Using the same (hyper-)parameters for all folds") - preds = cross_val_predict(clone(estimator).set_params(**est_params), x, y, cv=smpls, n_jobs=n_jobs, - method=method) - if method == 'predict_proba': - res['preds'] = preds[:, 1] + preds = cross_val_predict(clone(estimator).set_params(**est_params), x, y, cv=smpls, n_jobs=n_jobs, method=method) + if method == "predict_proba": + res["preds"] = preds[:, 1] else: - res['preds'] = preds - res['targets'] = np.copy(y) + res["preds"] = preds + res["targets"] = np.copy(y) else: if not smpls_is_partition: - assert not fold_specific_target, 'combination of fold-specific y and no cross-fitting not implemented yet' + assert not fold_specific_target, "combination of fold-specific y and no cross-fitting not implemented yet" assert len(smpls) == 1 - if method == 'predict_proba': + if method == "predict_proba": assert not fold_specific_target # fold_specific_target only needed for PLIV.partialXZ y = np.asarray(y) le = LabelEncoder() y = le.fit_transform(y) - parallel = Parallel(n_jobs=n_jobs, verbose=0, pre_dispatch='2*n_jobs') + parallel = Parallel(n_jobs=n_jobs, verbose=0, pre_dispatch="2*n_jobs") if fold_specific_target: y_list = list() @@ -93,19 +94,22 @@ def _dml_cv_predict(estimator, x, y, smpls=None, y_list = [y] * len(smpls) if est_params is None: - fitted_models = parallel(delayed(_fit)( - clone(estimator), x, y_list[idx], train_index, idx) - for idx, (train_index, test_index) in enumerate(smpls)) + fitted_models = parallel( + delayed(_fit)(clone(estimator), x, y_list[idx], train_index, idx) + for idx, (train_index, test_index) in enumerate(smpls) + ) elif isinstance(est_params, dict): # warnings.warn("Using the same (hyper-)parameters for all folds") - fitted_models = parallel(delayed(_fit)( - clone(estimator).set_params(**est_params), x, y_list[idx], train_index, idx) - for idx, (train_index, test_index) in enumerate(smpls)) + fitted_models = parallel( + delayed(_fit)(clone(estimator).set_params(**est_params), x, y_list[idx], train_index, idx) + for idx, (train_index, test_index) in enumerate(smpls) + ) else: - assert len(est_params) == len(smpls), 'provide one parameter setting per fold' - fitted_models = parallel(delayed(_fit)( - clone(estimator).set_params(**est_params[idx]), x, y_list[idx], train_index, idx) - for idx, (train_index, test_index) in enumerate(smpls)) + assert len(est_params) == len(smpls), "provide one parameter setting per fold" + fitted_models = parallel( + delayed(_fit)(clone(estimator).set_params(**est_params[idx]), x, y_list[idx], train_index, idx) + for idx, (train_index, test_index) in enumerate(smpls) + ) preds = np.full(n_obs, np.nan) targets = np.full(n_obs, np.nan) @@ -114,7 +118,7 @@ def _dml_cv_predict(estimator, x, y, smpls=None, for idx, (train_index, test_index) in enumerate(smpls): assert idx == fitted_models[idx][1] pred_fun = getattr(fitted_models[idx][0], method) - if method == 'predict_proba': + if method == "predict_proba": preds[test_index] = pred_fun(x[test_index, :])[:, 1] else: preds[test_index] = pred_fun(x[test_index, :]) @@ -129,58 +133,60 @@ def _dml_cv_predict(estimator, x, y, smpls=None, train_preds.append(pred_fun(x[train_index, :])) train_targets.append(y[train_index]) - res['preds'] = preds - res['targets'] = targets + res["preds"] = preds + res["targets"] = targets if return_train_preds: - res['train_preds'] = train_preds - res['train_targets'] = train_targets + res["train_preds"] = train_preds + res["train_targets"] = train_targets if return_models: fold_ids = [xx[1] for xx in fitted_models] if not np.all(fold_ids == np.arange(len(smpls))): - raise RuntimeError('export of fitted models failed') - res['models'] = [xx[0] for xx in fitted_models] + raise RuntimeError("export of fitted models failed") + res["models"] = [xx[0] for xx in fitted_models] return res -def _dml_tune(y, x, train_inds, - learner, param_grid, scoring_method, - n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search): +def _dml_tune( + y, x, train_inds, learner, param_grid, scoring_method, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search +): tune_res = list() for train_index in train_inds: tune_resampling = KFold(n_splits=n_folds_tune, shuffle=True) - if search_mode == 'grid_search': - g_grid_search = GridSearchCV(learner, param_grid, - scoring=scoring_method, - cv=tune_resampling, n_jobs=n_jobs_cv) + if search_mode == "grid_search": + g_grid_search = GridSearchCV(learner, param_grid, scoring=scoring_method, cv=tune_resampling, n_jobs=n_jobs_cv) else: - assert search_mode == 'randomized_search' - g_grid_search = RandomizedSearchCV(learner, param_grid, - scoring=scoring_method, - cv=tune_resampling, n_jobs=n_jobs_cv, - n_iter=n_iter_randomized_search) + assert search_mode == "randomized_search" + g_grid_search = RandomizedSearchCV( + learner, + param_grid, + scoring=scoring_method, + cv=tune_resampling, + n_jobs=n_jobs_cv, + n_iter=n_iter_randomized_search, + ) tune_res.append(g_grid_search.fit(x[train_index, :], y[train_index])) return tune_res def _draw_weights(method, n_rep_boot, n_obs): - if method == 'Bayes': - weights = np.random.exponential(scale=1.0, size=(n_rep_boot, n_obs)) - 1. - elif method == 'normal': + if method == "Bayes": + weights = np.random.exponential(scale=1.0, size=(n_rep_boot, n_obs)) - 1.0 + elif method == "normal": weights = np.random.normal(loc=0.0, scale=1.0, size=(n_rep_boot, n_obs)) - elif method == 'wild': + elif method == "wild": xx = np.random.normal(loc=0.0, scale=1.0, size=(n_rep_boot, n_obs)) yy = np.random.normal(loc=0.0, scale=1.0, size=(n_rep_boot, n_obs)) weights = xx / np.sqrt(2) + (np.power(yy, 2) - 1) / 2 else: - raise ValueError('invalid boot method') + raise ValueError("invalid boot method") return weights def _trimm(preds, trimming_rule, trimming_threshold): - if trimming_rule == 'truncate': + if trimming_rule == "truncate": preds[preds < trimming_threshold] = trimming_threshold preds[preds > 1 - trimming_threshold] = 1 - trimming_threshold return preds @@ -188,9 +194,10 @@ def _trimm(preds, trimming_rule, trimming_threshold): def _normalize_ipw(propensity, treatment): mean_treat1 = np.mean(np.divide(treatment, propensity)) - mean_treat0 = np.mean(np.divide(1.0-treatment, 1.0-propensity)) - normalized_weights = np.multiply(treatment, np.multiply(propensity, mean_treat1)) \ - + np.multiply(1.0-treatment, 1.0 - np.multiply(1.0-propensity, mean_treat0)) + mean_treat0 = np.mean(np.divide(1.0 - treatment, 1.0 - propensity)) + normalized_weights = np.multiply(treatment, np.multiply(propensity, mean_treat1)) + np.multiply( + 1.0 - treatment, 1.0 - np.multiply(1.0 - propensity, mean_treat0) + ) return normalized_weights @@ -228,14 +235,14 @@ def _get_bracket_guess(score, coef_start, coef_bounds): b_guess = (a, b) f_a = score(b_guess[0]) f_b = score(b_guess[1]) - s_different = (np.sign(f_a) != np.sign(f_b)) + s_different = np.sign(f_a) != np.sign(f_b) delta += 0.1 return s_different, b_guess def _default_kde(u, weights): dens = KDEUnivariate(u) - dens.fit(kernel='gau', bw='silverman', weights=weights, fft=False) + dens.fit(kernel="gau", bw="silverman", weights=weights, fft=False) return dens.evaluate(0) @@ -244,9 +251,7 @@ def _solve_ipw_score(ipw_score, bracket_guess): def abs_ipw_score(theta): return abs(ipw_score(theta)) - res = minimize_scalar(abs_ipw_score, - bracket=bracket_guess, - method='brent') + res = minimize_scalar(abs_ipw_score, bracket=bracket_guess, method="brent") ipw_est = res.x return ipw_est @@ -268,8 +273,7 @@ def _aggregate_coefs_and_ses(all_coefs, all_ses, var_scaling_factors): return coefs, ses -def _var_est(psi, psi_deriv, smpls, is_cluster_data, - cluster_vars=None, smpls_cluster=None, n_folds_per_cluster=None): +def _var_est(psi, psi_deriv, smpls, is_cluster_data, cluster_vars=None, smpls_cluster=None, n_folds_per_cluster=None): if not is_cluster_data: # psi and psi_deriv should be of shape (n_obs, ...) @@ -296,7 +300,7 @@ def _var_est(psi, psi_deriv, smpls, is_cluster_data, I_k = test_cluster_inds[0] const = 1 / len(I_k) for cluster_value in I_k: - ind_cluster = (first_cluster_var == cluster_value) + ind_cluster = first_cluster_var == cluster_value gamma_hat += const * np.sum(np.outer(psi[ind_cluster], psi[ind_cluster])) j_hat += np.sum(psi_deriv[test_inds]) / len(I_k) diff --git a/doubleml/utils/_plots.py b/doubleml/utils/_plots.py index 791203444..b5785272c 100644 --- a/doubleml/utils/_plots.py +++ b/doubleml/utils/_plots.py @@ -2,79 +2,92 @@ import plotly.graph_objects as go -def _sensitivity_contour_plot(x, - y, - contour_values, - unadjusted_value, - scenario_x, - scenario_y, - scenario_value, - include_scenario, - benchmarks=None, - fill=True): +def _sensitivity_contour_plot( + x, + y, + contour_values, + unadjusted_value, + scenario_x, + scenario_y, + scenario_value, + include_scenario, + benchmarks=None, + fill=True, +): if fill: - text_col = 'white' - contours_coloring = 'heatmap' + text_col = "white" + contours_coloring = "heatmap" else: - text_col = 'black' - contours_coloring = 'lines' + text_col = "black" + contours_coloring = "lines" # create figure - axis_names = ['cf_d', 'cf_y ', 'Bound'] + axis_names = ["cf_d", "cf_y ", "Bound"] fig = go.Figure() # basic contour plot - hov_temp = axis_names[0] + ': %{x:.3f}' + '
' + axis_names[1] + ': %{y:.3f}' + '' +\ - '
' + axis_names[2] - fig.add_trace(go.Contour(z=contour_values, - x=x, - y=y, - hovertemplate=hov_temp + ': %{z:.3f}' + '', - contours=dict(coloring=contours_coloring, - showlabels=True, - labelfont=dict(size=12, color=text_col)), - name='Contour')) + hov_temp = axis_names[0] + ": %{x:.3f}" + "
" + axis_names[1] + ": %{y:.3f}" + "" + "
" + axis_names[2] + fig.add_trace( + go.Contour( + z=contour_values, + x=x, + y=y, + hovertemplate=hov_temp + ": %{z:.3f}" + "", + contours=dict(coloring=contours_coloring, showlabels=True, labelfont=dict(size=12, color=text_col)), + name="Contour", + ) + ) if include_scenario: - fig.add_trace(go.Scatter(x=[scenario_x], - y=[scenario_y], - mode="markers+text", - marker=dict(size=10, color='red', line=dict(width=2, color=text_col)), - hovertemplate=hov_temp + f': {round(scenario_value, 3)}' + '', - name='Scenario', - textfont=dict(color=text_col, size=14), - text=['Scenario'], - textposition="top right", - showlegend=False)) + fig.add_trace( + go.Scatter( + x=[scenario_x], + y=[scenario_y], + mode="markers+text", + marker=dict(size=10, color="red", line=dict(width=2, color=text_col)), + hovertemplate=hov_temp + f": {round(scenario_value, 3)}" + "", + name="Scenario", + textfont=dict(color=text_col, size=14), + text=["Scenario"], + textposition="top right", + showlegend=False, + ) + ) # add unadjusted - fig.add_trace(go.Scatter(x=[0], - y=[0], - mode="markers+text", - marker=dict(size=10, color='red', line=dict(width=2, color=text_col)), - hovertemplate=hov_temp + f': {round(unadjusted_value, 3)}' + '', - name='Unadjusted', - text=['Unadjusted'], - textfont=dict(color=text_col, size=14), - textposition="top right", - showlegend=False)) + fig.add_trace( + go.Scatter( + x=[0], + y=[0], + mode="markers+text", + marker=dict(size=10, color="red", line=dict(width=2, color=text_col)), + hovertemplate=hov_temp + f": {round(unadjusted_value, 3)}" + "", + name="Unadjusted", + text=["Unadjusted"], + textfont=dict(color=text_col, size=14), + textposition="top right", + showlegend=False, + ) + ) # add benchmarks if benchmarks is not None: - fig.add_trace(go.Scatter(x=benchmarks['cf_d'], - y=benchmarks['cf_y'], - customdata=benchmarks['value'].reshape(-1, 1), - mode="markers+text", - marker=dict(size=10, color='red', line=dict(width=2, color=text_col)), - hovertemplate=hov_temp + ': %{customdata[0]:.3f}' + '', - name="Benchmarks", - textfont=dict(color=text_col, size=14), - text=list(map(lambda s: "" + s + "", benchmarks['name'])), - textposition="top right", - showlegend=False)) - fig.update_layout(title=None, - xaxis_title=axis_names[0], - yaxis_title=axis_names[1]) + fig.add_trace( + go.Scatter( + x=benchmarks["cf_d"], + y=benchmarks["cf_y"], + customdata=benchmarks["value"].reshape(-1, 1), + mode="markers+text", + marker=dict(size=10, color="red", line=dict(width=2, color=text_col)), + hovertemplate=hov_temp + ": %{customdata[0]:.3f}" + "", + name="Benchmarks", + textfont=dict(color=text_col, size=14), + text=list(map(lambda s: "" + s + "", benchmarks["name"])), + textposition="top right", + showlegend=False, + ) + ) + fig.update_layout(title=None, xaxis_title=axis_names[0], yaxis_title=axis_names[1]) fig.update_xaxes(range=[0, np.max(x)]) fig.update_yaxes(range=[0, np.max(y)]) diff --git a/doubleml/utils/blp.py b/doubleml/utils/blp.py index 159a74ed6..a671147a7 100644 --- a/doubleml/utils/blp.py +++ b/doubleml/utils/blp.py @@ -26,26 +26,21 @@ class DoubleMLBLP: Default is ``False``. """ - def __init__(self, - orth_signal, - basis, - is_gate=False): + def __init__(self, orth_signal, basis, is_gate=False): if not isinstance(orth_signal, np.ndarray): - raise TypeError('The signal must be of np.ndarray type. ' - f'Signal of type {str(type(orth_signal))} was passed.') + raise TypeError("The signal must be of np.ndarray type. " f"Signal of type {str(type(orth_signal))} was passed.") if orth_signal.ndim != 1: - raise ValueError('The signal must be of one dimensional. ' - f'Signal of dimensions {str(orth_signal.ndim)} was passed.') + raise ValueError( + "The signal must be of one dimensional. " f"Signal of dimensions {str(orth_signal.ndim)} was passed." + ) if not isinstance(basis, pd.DataFrame): - raise TypeError('The basis must be of DataFrame type. ' - f'Basis of type {str(type(basis))} was passed.') + raise TypeError("The basis must be of DataFrame type. " f"Basis of type {str(type(basis))} was passed.") if not basis.columns.is_unique: - raise ValueError('Invalid pd.DataFrame: ' - 'Contains duplicate column names.') + raise ValueError("Invalid pd.DataFrame: " "Contains duplicate column names.") self._orth_signal = orth_signal self._basis = basis @@ -57,10 +52,9 @@ def __init__(self, def __str__(self): class_name = self.__class__.__name__ - header = f'================== {class_name} Object ==================\n' + header = f"================== {class_name} Object ==================\n" fit_summary = str(self.summary) - res = header + \ - '\n------------------ Fit summary ------------------\n' + fit_summary + res = header + "\n------------------ Fit summary ------------------\n" + fit_summary return res @property @@ -96,21 +90,22 @@ def summary(self): """ A summary for the best linear predictor effect after calling :meth:`fit`. """ - col_names = ['coef', 'std err', 't', 'P>|t|', '[0.025', '0.975]'] + col_names = ["coef", "std err", "t", "P>|t|", "[0.025", "0.975]"] if self.blp_model is None: df_summary = pd.DataFrame(columns=col_names) else: - summary_stats = {'coef': self.blp_model.params, - 'std err': self.blp_model.bse, - 't': self.blp_model.tvalues, - 'P>|t|': self.blp_model.pvalues, - '[0.025': self.blp_model.conf_int()[0], - '0.975]': self.blp_model.conf_int()[1]} - df_summary = pd.DataFrame(summary_stats, - columns=col_names) + summary_stats = { + "coef": self.blp_model.params, + "std err": self.blp_model.bse, + "t": self.blp_model.tvalues, + "P>|t|": self.blp_model.pvalues, + "[0.025": self.blp_model.conf_int()[0], + "0.975]": self.blp_model.conf_int()[1], + } + df_summary = pd.DataFrame(summary_stats, columns=col_names) return df_summary - def fit(self, cov_type='HC0', **kwargs): + def fit(self, cov_type="HC0", **kwargs): """ Estimate DoubleMLBLP models. @@ -164,25 +159,25 @@ def confint(self, basis=None, joint=False, level=0.95, n_rep_boot=500): A data frame with the confidence interval(s). """ if not isinstance(joint, bool): - raise TypeError('joint must be True or False. ' - f'Got {str(joint)}.') + raise TypeError("joint must be True or False. " f"Got {str(joint)}.") if not isinstance(level, float): - raise TypeError('The confidence level must be of float type. ' - f'{str(level)} of type {str(type(level))} was passed.') + raise TypeError( + "The confidence level must be of float type. " f"{str(level)} of type {str(type(level))} was passed." + ) if (level <= 0) | (level >= 1): - raise ValueError('The confidence level must be in (0,1). ' - f'{str(level)} was passed.') + raise ValueError("The confidence level must be in (0,1). " f"{str(level)} was passed.") if not isinstance(n_rep_boot, int): - raise TypeError('The number of bootstrap replications must be of int type. ' - f'{str(n_rep_boot)} of type {str(type(n_rep_boot))} was passed.') + raise TypeError( + "The number of bootstrap replications must be of int type. " + f"{str(n_rep_boot)} of type {str(type(n_rep_boot))} was passed." + ) if n_rep_boot < 1: - raise ValueError('The number of bootstrap replications must be positive. ' - f'{str(n_rep_boot)} was passed.') + raise ValueError("The number of bootstrap replications must be positive. " f"{str(n_rep_boot)} was passed.") if self._blp_model is None: - raise ValueError('Apply fit() before confint().') + raise ValueError("Apply fit() before confint().") alpha = 1 - level gate_names = None @@ -194,23 +189,26 @@ def confint(self, basis=None, joint=False, level=0.95, n_rep_boot=500): gate_names = list(self._basis.columns.values) else: if joint: - warnings.warn('Returning pointwise confidence intervals for basis coefficients.', UserWarning) + warnings.warn("Returning pointwise confidence intervals for basis coefficients.", UserWarning) # return the confidence intervals for the basis coefficients - ci = np.vstack(( - self.blp_model.conf_int(alpha=alpha/2)[0], - self.blp_model.params, - self.blp_model.conf_int(alpha=alpha/2)[1]) - ).T + ci = np.vstack( + ( + self.blp_model.conf_int(alpha=alpha / 2)[0], + self.blp_model.params, + self.blp_model.conf_int(alpha=alpha / 2)[1], + ) + ).T df_ci = pd.DataFrame( ci, - columns=['{:.1f} %'.format(alpha/2 * 100), 'effect', '{:.1f} %'.format((1-alpha/2) * 100)], - index=self._basis.columns) + columns=["{:.1f} %".format(alpha / 2 * 100), "effect", "{:.1f} %".format((1 - alpha / 2) * 100)], + index=self._basis.columns, + ) return df_ci elif not (basis.shape[1] == self._basis.shape[1]): - raise ValueError('Invalid basis: DataFrame has to have the exact same number and ordering of columns.') + raise ValueError("Invalid basis: DataFrame has to have the exact same number and ordering of columns.") elif not list(basis.columns.values) == list(self._basis.columns.values): - raise ValueError('Invalid basis: DataFrame has to have the exact same number and ordering of columns.') + raise ValueError("Invalid basis: DataFrame has to have the exact same number and ordering of columns.") # blp of the orthogonal signal g_hat = self._blp_model.predict(basis) @@ -222,8 +220,7 @@ def confint(self, basis=None, joint=False, level=0.95, n_rep_boot=500): if joint: # calculate the maximum t-statistic with bootstrap normal_samples = np.random.normal(size=[basis.shape[1], n_rep_boot]) - bootstrap_samples = np.multiply(np.dot(np_basis, np.dot(sqrtm(self._blp_omega), normal_samples)).T, - (1.0 / blp_se)) + bootstrap_samples = np.multiply(np.dot(np_basis, np.dot(sqrtm(self._blp_omega), normal_samples)).T, (1.0 / blp_se)) max_t_stat = np.quantile(np.max(np.abs(bootstrap_samples), axis=0), q=level) @@ -239,9 +236,11 @@ def confint(self, basis=None, joint=False, level=0.95, n_rep_boot=500): g_hat_upper = g_hat + norm.ppf(q=1 - alpha / 2) * blp_se ci = np.vstack((g_hat_lower, g_hat, g_hat_upper)).T - df_ci = pd.DataFrame(ci, - columns=['{:.1f} %'.format(alpha/2 * 100), 'effect', '{:.1f} %'.format((1-alpha/2) * 100)], - index=basis.index) + df_ci = pd.DataFrame( + ci, + columns=["{:.1f} %".format(alpha / 2 * 100), "effect", "{:.1f} %".format((1 - alpha / 2) * 100)], + index=basis.index, + ) if self._is_gate and gate_names is not None: df_ci.index = gate_names diff --git a/doubleml/utils/gain_statistics.py b/doubleml/utils/gain_statistics.py index 2fa233b33..79ff8406c 100644 --- a/doubleml/utils/gain_statistics.py +++ b/doubleml/utils/gain_statistics.py @@ -25,56 +25,76 @@ def gain_statistics(dml_long, dml_short): sensitivity_elements_short = dml_short.framework.sensitivity_elements if not isinstance(sensitivity_elements_long, dict): - raise TypeError("dml_long does not contain the necessary sensitivity elements. " - "Expected dict for dml_long.framework.sensitivity_elements.") - expected_keys = ['sigma2', 'nu2'] + raise TypeError( + "dml_long does not contain the necessary sensitivity elements. " + "Expected dict for dml_long.framework.sensitivity_elements." + ) + expected_keys = ["sigma2", "nu2"] if not all(key in sensitivity_elements_long.keys() for key in expected_keys): - raise ValueError("dml_long does not contain the necessary sensitivity elements. " - "Required keys are: " + str(expected_keys)) + raise ValueError( + "dml_long does not contain the necessary sensitivity elements. " "Required keys are: " + str(expected_keys) + ) if not isinstance(sensitivity_elements_short, dict): - raise TypeError("dml_short does not contain the necessary sensitivity elements. " - "Expected dict for dml_short.framework.sensitivity_elements.") + raise TypeError( + "dml_short does not contain the necessary sensitivity elements. " + "Expected dict for dml_short.framework.sensitivity_elements." + ) if not all(key in sensitivity_elements_short.keys() for key in expected_keys): - raise ValueError("dml_short does not contain the necessary sensitivity elements. " - "Required keys are: " + str(expected_keys)) + raise ValueError( + "dml_short does not contain the necessary sensitivity elements. " "Required keys are: " + str(expected_keys) + ) for key in expected_keys: if not isinstance(sensitivity_elements_long[key], np.ndarray): - raise TypeError("dml_long does not contain the necessary sensitivity elements. " - f"Expected numpy.ndarray for key {key}.") + raise TypeError( + "dml_long does not contain the necessary sensitivity elements. " f"Expected numpy.ndarray for key {key}." + ) if not isinstance(sensitivity_elements_short[key], np.ndarray): - raise TypeError("dml_short does not contain the necessary sensitivity elements. " - f"Expected numpy.ndarray for key {key}.") + raise TypeError( + "dml_short does not contain the necessary sensitivity elements. " f"Expected numpy.ndarray for key {key}." + ) if len(sensitivity_elements_long[key].shape) != 3 or sensitivity_elements_long[key].shape[0] != 1: - raise ValueError("dml_long does not contain the necessary sensitivity elements. " - f"Expected 3 dimensions of shape (1, n_coef, n_rep) for key {key}.") + raise ValueError( + "dml_long does not contain the necessary sensitivity elements. " + f"Expected 3 dimensions of shape (1, n_coef, n_rep) for key {key}." + ) if len(sensitivity_elements_short[key].shape) != 3 or sensitivity_elements_short[key].shape[0] != 1: - raise ValueError("dml_short does not contain the necessary sensitivity elements. " - f"Expected 3 dimensions of shape (1, n_coef, n_rep) for key {key}.") + raise ValueError( + "dml_short does not contain the necessary sensitivity elements. " + f"Expected 3 dimensions of shape (1, n_coef, n_rep) for key {key}." + ) if not np.array_equal(sensitivity_elements_long[key].shape, sensitivity_elements_short[key].shape): - raise ValueError("dml_long and dml_short do not contain the same shape of sensitivity elements. " - "Shapes of " + key + " are: " + str(sensitivity_elements_long[key].shape) + - " and " + str(sensitivity_elements_short[key].shape)) + raise ValueError( + "dml_long and dml_short do not contain the same shape of sensitivity elements. " + "Shapes of " + + key + + " are: " + + str(sensitivity_elements_long[key].shape) + + " and " + + str(sensitivity_elements_short[key].shape) + ) if not isinstance(dml_long.all_coef, np.ndarray): raise TypeError("dml_long.all_coef does not contain the necessary coefficients. Expected numpy.ndarray.") if not isinstance(dml_short.all_coef, np.ndarray): raise TypeError("dml_short.all_coef does not contain the necessary coefficients. Expected numpy.ndarray.") - expected_shape = (sensitivity_elements_long['sigma2'].shape[1], sensitivity_elements_long['sigma2'].shape[2]) + expected_shape = (sensitivity_elements_long["sigma2"].shape[1], sensitivity_elements_long["sigma2"].shape[2]) if dml_long.all_coef.shape != expected_shape: - raise ValueError("dml_long.all_coef does not contain the necessary coefficients. Expected shape: " + - str(expected_shape)) + raise ValueError( + "dml_long.all_coef does not contain the necessary coefficients. Expected shape: " + str(expected_shape) + ) if dml_short.all_coef.shape != expected_shape: - raise ValueError("dml_short.all_coef does not contain the necessary coefficients. Expected shape: " + - str(expected_shape)) + raise ValueError( + "dml_short.all_coef does not contain the necessary coefficients. Expected shape: " + str(expected_shape) + ) # save elements for readability var_y = np.var(dml_long._dml_data.y) - var_y_residuals_long = np.squeeze(sensitivity_elements_long['sigma2'], axis=0) - nu2_long = np.squeeze(sensitivity_elements_long['nu2'], axis=0) - var_y_residuals_short = np.squeeze(sensitivity_elements_short['sigma2'], axis=0) - nu2_short = np.squeeze(sensitivity_elements_short['nu2'], axis=0) + var_y_residuals_long = np.squeeze(sensitivity_elements_long["sigma2"], axis=0) + nu2_long = np.squeeze(sensitivity_elements_long["nu2"], axis=0) + var_y_residuals_short = np.squeeze(sensitivity_elements_short["sigma2"], axis=0) + nu2_short = np.squeeze(sensitivity_elements_short["nu2"], axis=0) # compute nonparametric R2 R2_y_long = 1.0 - np.divide(var_y_residuals_long, var_y) @@ -96,11 +116,9 @@ def gain_statistics(dml_long, dml_short): var_riesz = nu2_long - nu2_short denom = np.sqrt(np.multiply(var_g, var_riesz), out=np.zeros_like(var_g), where=(var_g > 0) & (var_riesz > 0)) rho_sign = np.sign(all_delta_theta) - rho_values = np.clip(np.divide(np.absolute(all_delta_theta), - denom, - out=np.ones_like(all_delta_theta), - where=denom != 0), - 0.0, 1.0) + rho_values = np.clip( + np.divide(np.absolute(all_delta_theta), denom, out=np.ones_like(all_delta_theta), where=denom != 0), 0.0, 1.0 + ) all_rho_benchmark = np.multiply(rho_values, rho_sign) rho_benchmark = np.median(all_rho_benchmark, axis=1) benchmark_dict = { diff --git a/doubleml/utils/global_learner.py b/doubleml/utils/global_learner.py index 250f7efb3..4acc87357 100644 --- a/doubleml/utils/global_learner.py +++ b/doubleml/utils/global_learner.py @@ -11,10 +11,11 @@ class GlobalRegressor(BaseEstimator, RegressorMixin): base_estimator: regressor implementing ``fit()`` and ``predict()`` Regressor that is used when ``fit()`` ``predict()`` and ``predict_proba()`` are being called. """ + def __init__(self, base_estimator): if not is_regressor(base_estimator): - raise ValueError(f'base_estimator must be a regressor. Got {base_estimator.__class__.__name__} instead.') + raise ValueError(f"base_estimator must be a regressor. Got {base_estimator.__class__.__name__} instead.") self.base_estimator = base_estimator @@ -59,10 +60,11 @@ class GlobalClassifier(BaseEstimator, ClassifierMixin): base_estimator: classifier implementing ``fit()`` and ``predict_proba()`` Classifier that is used when ``fit()``, ``predict()`` and ``predict_proba()`` are being called. """ + def __init__(self, base_estimator): if not is_classifier(base_estimator): - raise ValueError(f'base_estimator must be a classifier. Got {base_estimator.__class__.__name__} instead.') + raise ValueError(f"base_estimator must be a classifier. Got {base_estimator.__class__.__name__} instead.") self.base_estimator = base_estimator diff --git a/doubleml/utils/policytree.py b/doubleml/utils/policytree.py index 59ee63ecc..59ae0cf9b 100644 --- a/doubleml/utils/policytree.py +++ b/doubleml/utils/policytree.py @@ -28,46 +28,38 @@ class DoubleMLPolicyTree: """ - def __init__(self, - orth_signal, - features, - depth=2, - **tree_params): + def __init__(self, orth_signal, features, depth=2, **tree_params): if not isinstance(orth_signal, np.ndarray): - raise TypeError('The signal must be of np.ndarray type. ' - f'Signal of type {str(type(orth_signal))} was passed.') + raise TypeError("The signal must be of np.ndarray type. " f"Signal of type {str(type(orth_signal))} was passed.") if orth_signal.ndim != 1: - raise ValueError('The signal must be of one dimensional. ' - f'Signal of dimensions {str(orth_signal.ndim)} was passed.') + raise ValueError( + "The signal must be of one dimensional. " f"Signal of dimensions {str(orth_signal.ndim)} was passed." + ) if not isinstance(features, pd.DataFrame): - raise TypeError('The features must be of DataFrame type. ' - f'Features of type {str(type(features))} was passed.') + raise TypeError("The features must be of DataFrame type. " f"Features of type {str(type(features))} was passed.") if not features.columns.is_unique: - raise ValueError('Invalid pd.DataFrame: ' - 'Contains duplicate column names.') + raise ValueError("Invalid pd.DataFrame: " "Contains duplicate column names.") self._orth_signal = orth_signal self._features = features self._depth = depth self._tree_params = tree_params - self._tree_params.setdefault("ccp_alpha", .01) + self._tree_params.setdefault("ccp_alpha", 0.01) self._tree_params.setdefault("min_samples_leaf", 8) # initialize tree - self._policy_tree = DecisionTreeClassifier(max_depth=self._depth, - **self._tree_params) + self._policy_tree = DecisionTreeClassifier(max_depth=self._depth, **self._tree_params) def __str__(self): class_name = self.__class__.__name__ - header = f'================== {class_name} Object ==================\n' + header = f"================== {class_name} Object ==================\n" fit_summary = str(self.summary) - res = header + \ - '\n------------------ Summary ------------------\n' + fit_summary + res = header + "\n------------------ Summary ------------------\n" + fit_summary return res @property @@ -112,8 +104,7 @@ def fit(self): # fit the tree with target binary score, sample weights absolute score and # provided feature variables - self._policy_tree.fit(X=self._features, y=bin_signal, - sample_weight=abs_signal) + self._policy_tree.fit(X=self._features, y=bin_signal, sample_weight=abs_signal) return self @@ -125,10 +116,15 @@ def plot_tree(self): ------- self : object """ - check_is_fitted(self._policy_tree, msg='Policy Tree not yet fitted. Call fit before plot_tree.') - - artists = plot_tree(self.policy_tree, feature_names=list(self._features.keys()), filled=True, - class_names=["No Treatment", "Treatment"], impurity=False) + check_is_fitted(self._policy_tree, msg="Policy Tree not yet fitted. Call fit before plot_tree.") + + artists = plot_tree( + self.policy_tree, + feature_names=list(self._features.keys()), + filled=True, + class_names=["No Treatment", "Treatment"], + impurity=False, + ) return artists def predict(self, features): @@ -146,15 +142,16 @@ def predict(self, features): ------- self : object """ - check_is_fitted(self._policy_tree, msg='Policy Tree not yet fitted. Call fit before predict.') + check_is_fitted(self._policy_tree, msg="Policy Tree not yet fitted. Call fit before predict.") if not isinstance(features, pd.DataFrame): - raise TypeError('The features must be of DataFrame type. ' - f'Features of type {str(type(features))} was passed.') + raise TypeError("The features must be of DataFrame type. " f"Features of type {str(type(features))} was passed.") if not set(features.keys()) == set(self._features.keys()): - raise KeyError(f'The features must have the keys {self._features.keys()}. ' - f'Features with keys {features.keys()} were passed.') + raise KeyError( + f"The features must have the keys {self._features.keys()}. " + f"Features with keys {features.keys()} were passed." + ) predictions = self.policy_tree.predict(features) diff --git a/doubleml/utils/resampling.py b/doubleml/utils/resampling.py index 2d8b1e971..6345ed9a6 100644 --- a/doubleml/utils/resampling.py +++ b/doubleml/utils/resampling.py @@ -3,19 +3,16 @@ class DoubleMLResampling: - def __init__(self, - n_folds, - n_rep, - n_obs, - stratify=None): + def __init__(self, n_folds, n_rep, n_obs, stratify=None): self.n_folds = n_folds self.n_rep = n_rep self.n_obs = n_obs self.stratify = stratify if n_folds < 2: - raise ValueError('n_folds must be greater than 1. ' - 'You can use set_sample_splitting with a tuple to only use one fold.') + raise ValueError( + "n_folds must be greater than 1. " "You can use set_sample_splitting with a tuple to only use one fold." + ) if self.stratify is None: self.resampling = RepeatedKFold(n_splits=n_folds, n_repeats=n_rep) @@ -24,18 +21,12 @@ def __init__(self, def split_samples(self): all_smpls = [(train, test) for train, test in self.resampling.split(X=np.zeros(self.n_obs), y=self.stratify)] - smpls = [all_smpls[(i_repeat * self.n_folds):((i_repeat + 1) * self.n_folds)] - for i_repeat in range(self.n_rep)] + smpls = [all_smpls[(i_repeat * self.n_folds) : ((i_repeat + 1) * self.n_folds)] for i_repeat in range(self.n_rep)] return smpls class DoubleMLClusterResampling: - def __init__(self, - n_folds, - n_rep, - n_obs, - n_cluster_vars, - cluster_vars): + def __init__(self, n_folds, n_rep, n_obs, n_cluster_vars, cluster_vars): self.n_folds = n_folds self.n_rep = n_rep @@ -56,14 +47,16 @@ def split_samples(self): this_cluster_var = self.cluster_vars[:, i_var] clusters = np.unique(this_cluster_var) n_clusters = len(clusters) - smpls_cluster_vars.append([(clusters[train], clusters[test]) - for train, test in self.resampling.split(np.zeros(n_clusters))]) + smpls_cluster_vars.append( + [(clusters[train], clusters[test]) for train, test in self.resampling.split(np.zeros(n_clusters))] + ) smpls = [] smpls_cluster = [] # build the cartesian product - cart = np.array(np.meshgrid(*[np.arange(self.n_folds) - for i in range(self.n_cluster_vars)])).T.reshape(-1, self.n_cluster_vars) + cart = np.array(np.meshgrid(*[np.arange(self.n_folds) for i in range(self.n_cluster_vars)])).T.reshape( + -1, self.n_cluster_vars + ) for i_smpl in range(cart.shape[0]): ind_train = np.full(self.n_obs, True) ind_test = np.full(self.n_obs, True) diff --git a/doubleml/utils/tests/_utils_blp_manual.py b/doubleml/utils/tests/_utils_blp_manual.py index 00985facd..923d9eea3 100644 --- a/doubleml/utils/tests/_utils_blp_manual.py +++ b/doubleml/utils/tests/_utils_blp_manual.py @@ -38,8 +38,7 @@ def blp_confint(blp_model, basis, joint=False, level=0.95, n_rep_boot=500): g_hat_upper = g_hat + norm.ppf(q=1 - alpha / 2) * blp_se ci = np.vstack((g_hat_lower, g_hat, g_hat_upper)).T - df_ci = pd.DataFrame(ci, - columns=['{:.1f} %'.format(alpha / 2 * 100), 'effect', - '{:.1f} %'.format((1 - alpha / 2) * 100)], - index=basis.index) + df_ci = pd.DataFrame( + ci, columns=["{:.1f} %".format(alpha / 2 * 100), "effect", "{:.1f} %".format((1 - alpha / 2) * 100)], index=basis.index + ) return df_ci diff --git a/doubleml/utils/tests/_utils_pt_manual.py b/doubleml/utils/tests/_utils_pt_manual.py index 2af0d02f4..dabaf2c76 100644 --- a/doubleml/utils/tests/_utils_pt_manual.py +++ b/doubleml/utils/tests/_utils_pt_manual.py @@ -3,10 +3,8 @@ def fit_policytree(orth_signal, features, depth): - policytree_model = DecisionTreeClassifier(max_depth=depth, - ccp_alpha=.01, - min_samples_leaf=8).fit(X=features, - y=(np.sign(orth_signal) + 1) / 2, - sample_weight=np.abs(orth_signal)) + policytree_model = DecisionTreeClassifier(max_depth=depth, ccp_alpha=0.01, min_samples_leaf=8).fit( + X=features, y=(np.sign(orth_signal) + 1) / 2, sample_weight=np.abs(orth_signal) + ) return policytree_model diff --git a/doubleml/utils/tests/test_blp.py b/doubleml/utils/tests/test_blp.py index 0a8e51025..d2faa4401 100644 --- a/doubleml/utils/tests/test_blp.py +++ b/doubleml/utils/tests/test_blp.py @@ -9,38 +9,34 @@ from ._utils_blp_manual import blp_confint, fit_blp -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def ci_joint(request): return request.param -@pytest.fixture(scope='module', - params=[0.95, 0.9]) +@pytest.fixture(scope="module", params=[0.95, 0.9]) def ci_level(request): return request.param -@pytest.fixture(scope='module', - params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) +@pytest.fixture(scope="module", params=["nonrobust", "HC0", "HC1", "HC2", "HC3"]) def cov_type(request): return request.param -@pytest.fixture(scope='module', - params=[True, False]) +@pytest.fixture(scope="module", params=[True, False]) def use_t(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_blp_fixture(ci_joint, ci_level, cov_type, use_t): n = 50 - kwargs = {'cov_type': cov_type, 'use_t': use_t} + kwargs = {"cov_type": cov_type, "use_t": use_t} np.random.seed(42) random_basis = pd.DataFrame(np.random.normal(0, 1, size=(n, 3))) - random_signal = np.random.normal(0, 1, size=(n, )) + random_signal = np.random.normal(0, 1, size=(n,)) blp = dml.DoubleMLBLP(random_signal, random_basis) @@ -52,72 +48,67 @@ def dml_blp_fixture(ci_joint, ci_level, cov_type, use_t): ci_1 = blp.confint(random_basis, joint=ci_joint, level=ci_level, n_rep_boot=1000) np.random.seed(42) ci_2 = blp.confint(joint=ci_joint, level=ci_level, n_rep_boot=1000) - expected_ci_2 = np.vstack(( - blp.blp_model.conf_int(alpha=(1-ci_level)/2)[0], - blp.blp_model.params, - blp.blp_model.conf_int(alpha=(1-ci_level)/2)[1])).T + expected_ci_2 = np.vstack( + ( + blp.blp_model.conf_int(alpha=(1 - ci_level) / 2)[0], + blp.blp_model.params, + blp.blp_model.conf_int(alpha=(1 - ci_level) / 2)[1], + ) + ).T np.random.seed(42) ci_manual = blp_confint(blp_manual, random_basis, joint=ci_joint, level=ci_level, n_rep_boot=1000) - res_dict = {'coef': blp.blp_model.params, - 'coef_manual': blp_manual.params, - 'values': blp.blp_model.fittedvalues, - 'values_manual': blp_manual.fittedvalues, - 'omega': blp.blp_omega, - 'omega_manual': blp_manual.cov_params().to_numpy(), - 'basis': blp.basis, - 'signal': blp.orth_signal, - 'ci_1': ci_1, - 'ci_2': ci_2, - 'expected_ci_2': expected_ci_2, - 'ci_manual': ci_manual, - 'blp_model': blp, - 'unfitted_blp_model': blp_obj} + res_dict = { + "coef": blp.blp_model.params, + "coef_manual": blp_manual.params, + "values": blp.blp_model.fittedvalues, + "values_manual": blp_manual.fittedvalues, + "omega": blp.blp_omega, + "omega_manual": blp_manual.cov_params().to_numpy(), + "basis": blp.basis, + "signal": blp.orth_signal, + "ci_1": ci_1, + "ci_2": ci_2, + "expected_ci_2": expected_ci_2, + "ci_manual": ci_manual, + "blp_model": blp, + "unfitted_blp_model": blp_obj, + } return res_dict @pytest.mark.ci def test_dml_blp_coef(dml_blp_fixture): - assert np.allclose(dml_blp_fixture['coef'], - dml_blp_fixture['coef_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_blp_fixture["coef"], dml_blp_fixture["coef_manual"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_blp_values(dml_blp_fixture): - assert np.allclose(dml_blp_fixture['values'], - dml_blp_fixture['values_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_blp_fixture["values"], dml_blp_fixture["values_manual"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_blp_omega(dml_blp_fixture): - assert np.allclose(dml_blp_fixture['omega'], - dml_blp_fixture['omega_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_blp_fixture["omega"], dml_blp_fixture["omega_manual"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_blp_ci_2(dml_blp_fixture): - assert np.allclose(dml_blp_fixture['expected_ci_2'], - dml_blp_fixture['ci_2'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_blp_fixture["expected_ci_2"], dml_blp_fixture["ci_2"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_blp_ci_1(dml_blp_fixture): - assert np.allclose(dml_blp_fixture['ci_1'], - dml_blp_fixture['ci_manual'], - rtol=1e-9, atol=1e-4) + assert np.allclose(dml_blp_fixture["ci_1"], dml_blp_fixture["ci_manual"], rtol=1e-9, atol=1e-4) @pytest.mark.ci def test_dml_blp_return_types(dml_blp_fixture): - assert isinstance(dml_blp_fixture['blp_model'].__str__(), str) - assert isinstance(dml_blp_fixture['blp_model'].summary, pd.DataFrame) - assert isinstance(dml_blp_fixture['unfitted_blp_model'].summary, pd.DataFrame) + assert isinstance(dml_blp_fixture["blp_model"].__str__(), str) + assert isinstance(dml_blp_fixture["blp_model"].summary, pd.DataFrame) + assert isinstance(dml_blp_fixture["unfitted_blp_model"].summary, pd.DataFrame) @pytest.mark.ci @@ -125,14 +116,12 @@ def test_dml_blp_defaults(): n = 50 np.random.seed(42) random_basis = pd.DataFrame(np.random.normal(0, 1, size=(n, 3))) - random_signal = np.random.normal(0, 1, size=(n, )) + random_signal = np.random.normal(0, 1, size=(n,)) blp = dml.DoubleMLBLP(random_signal, random_basis) blp.fit() - assert np.allclose(blp.blp_omega, - blp.blp_model.cov_HC0, - rtol=1e-9, atol=1e-4) + assert np.allclose(blp.blp_omega, blp.blp_model.cov_HC0, rtol=1e-9, atol=1e-4) assert blp._is_gate is False @@ -145,41 +134,40 @@ def test_doubleml_exception_blp(): msg = "The signal must be of np.ndarray type. Signal of type was passed." with pytest.raises(TypeError, match=msg): dml.DoubleMLBLP(orth_signal=1, basis=random_basis) - msg = 'The signal must be of one dimensional. Signal of dimensions 2 was passed.' + msg = "The signal must be of one dimensional. Signal of dimensions 2 was passed." with pytest.raises(ValueError, match=msg): dml.DoubleMLBLP(orth_signal=np.array([[1], [2]]), basis=random_basis) msg = "The basis must be of DataFrame type. Basis of type was passed." with pytest.raises(TypeError, match=msg): dml.DoubleMLBLP(orth_signal=signal, basis=1) - msg = 'Invalid pd.DataFrame: Contains duplicate column names.' + msg = "Invalid pd.DataFrame: Contains duplicate column names." with pytest.raises(ValueError, match=msg): - dml.DoubleMLBLP(orth_signal=signal, basis=pd.DataFrame(np.array([[1, 2], [4, 5]]), - columns=['a_1', 'a_1'])) + dml.DoubleMLBLP(orth_signal=signal, basis=pd.DataFrame(np.array([[1, 2], [4, 5]]), columns=["a_1", "a_1"])) dml_blp_confint = dml.DoubleMLBLP(orth_signal=signal, basis=random_basis) - msg = r'Apply fit\(\) before confint\(\).' + msg = r"Apply fit\(\) before confint\(\)." with pytest.raises(ValueError, match=msg): dml_blp_confint.confint(random_basis) dml_blp_confint.fit() - msg = 'joint must be True or False. Got 1.' + msg = "joint must be True or False. Got 1." with pytest.raises(TypeError, match=msg): dml_blp_confint.confint(random_basis, joint=1) msg = "The confidence level must be of float type. 5% of type was passed." with pytest.raises(TypeError, match=msg): - dml_blp_confint.confint(random_basis, level='5%') - msg = r'The confidence level must be in \(0,1\). 0.0 was passed.' + dml_blp_confint.confint(random_basis, level="5%") + msg = r"The confidence level must be in \(0,1\). 0.0 was passed." with pytest.raises(ValueError, match=msg): - dml_blp_confint.confint(random_basis, level=0.) + dml_blp_confint.confint(random_basis, level=0.0) msg = "The number of bootstrap replications must be of int type. 500 of type was passed." with pytest.raises(TypeError, match=msg): - dml_blp_confint.confint(random_basis, n_rep_boot='500') - msg = 'The number of bootstrap replications must be positive. 0 was passed.' + dml_blp_confint.confint(random_basis, n_rep_boot="500") + msg = "The number of bootstrap replications must be positive. 0 was passed." with pytest.raises(ValueError, match=msg): dml_blp_confint.confint(random_basis, n_rep_boot=0) - msg = 'Invalid basis: DataFrame has to have the exact same number and ordering of columns.' + msg = "Invalid basis: DataFrame has to have the exact same number and ordering of columns." with pytest.raises(ValueError, match=msg): - dml_blp_confint.confint(basis=pd.DataFrame(np.array([[1], [4]]), columns=['a_1'])) - msg = 'Invalid basis: DataFrame has to have the exact same number and ordering of columns.' + dml_blp_confint.confint(basis=pd.DataFrame(np.array([[1], [4]]), columns=["a_1"])) + msg = "Invalid basis: DataFrame has to have the exact same number and ordering of columns." with pytest.raises(ValueError, match=msg): - dml_blp_confint.confint(basis=pd.DataFrame(np.array([[1, 2, 3], [4, 5, 6]]), columns=['x_1', 'x_2', 'x_3'])) + dml_blp_confint.confint(basis=pd.DataFrame(np.array([[1, 2, 3], [4, 5, 6]]), columns=["x_1", "x_2", "x_3"])) diff --git a/doubleml/utils/tests/test_exceptions_gain_statistics.py b/doubleml/utils/tests/test_exceptions_gain_statistics.py index a7bcc7f05..c434c689b 100644 --- a/doubleml/utils/tests/test_exceptions_gain_statistics.py +++ b/doubleml/utils/tests/test_exceptions_gain_statistics.py @@ -4,12 +4,12 @@ from doubleml.utils.gain_statistics import gain_statistics -class test_framework(): +class test_framework: def __init__(self, sensitivity_elements): self.sensitivity_elements = sensitivity_elements -class test_dml_class(): +class test_dml_class: def __init__(self, sensitivity_elements, all_coef): self.framework = test_framework(sensitivity_elements) self.all_coef = all_coef @@ -24,17 +24,16 @@ def __init__(self, sensitivity_elements, all_coef): def test_doubleml_exception_data(): dml_correct = test_dml_class( sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - 'nu2': np.random.normal(size=(n_obs, n_rep, n_coef)) + "sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), + "nu2": np.random.normal(size=(n_obs, n_rep, n_coef)), }, - all_coef=np.random.normal(size=(n_rep, n_coef)) + all_coef=np.random.normal(size=(n_rep, n_coef)), ) # incorrect types dml_incorrect = test_dml_class( - sensitivity_elements=np.random.normal(size=(n_obs, n_rep, n_coef)), - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) + sensitivity_elements=np.random.normal(size=(n_obs, n_rep, n_coef)), all_coef=np.random.normal(size=(n_rep, n_coef)) + ) msg = r"dml_long does not contain the necessary sensitivity elements\. " msg += r"Expected dict for dml_long\.framework\.sensitivity_elements\." with pytest.raises(TypeError, match=msg): @@ -46,11 +45,11 @@ def test_doubleml_exception_data(): # incorrect keys dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), + }, + all_coef=np.random.normal(size=(n_rep, n_coef)), + ) msg = r"dml_long does not contain the necessary sensitivity elements\. Required keys are: \['sigma2', 'nu2'\]" with pytest.raises(ValueError, match=msg): _ = gain_statistics(dml_incorrect, dml_correct) @@ -60,12 +59,9 @@ def test_doubleml_exception_data(): # incorrect type for keys dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': {}, - 'nu2': np.random.normal(size=(n_obs, n_rep, n_coef)) - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) + sensitivity_elements={"sigma2": {}, "nu2": np.random.normal(size=(n_obs, n_rep, n_coef))}, + all_coef=np.random.normal(size=(n_rep, n_coef)), + ) msg = r"dml_long does not contain the necessary sensitivity elements\. Expected numpy\.ndarray for key sigma2\." with pytest.raises(TypeError, match=msg): _ = gain_statistics(dml_incorrect, dml_correct) @@ -74,11 +70,8 @@ def test_doubleml_exception_data(): _ = gain_statistics(dml_correct, dml_incorrect) dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - 'nu2': {} - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) + sensitivity_elements={"sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), "nu2": {}}, + all_coef=np.random.normal(size=(n_rep, n_coef)), ) msg = r"dml_long does not contain the necessary sensitivity elements\. Expected numpy\.ndarray for key nu2\." with pytest.raises(TypeError, match=msg): @@ -89,45 +82,53 @@ def test_doubleml_exception_data(): # incorrect shape for keys dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs + 1, n_rep, n_coef)), - 'nu2': np.random.normal(size=(n_obs, n_rep, n_coef)) - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) - msg = (r"dml_long does not contain the necessary sensitivity elements\. " - r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key sigma2\.") + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs + 1, n_rep, n_coef)), + "nu2": np.random.normal(size=(n_obs, n_rep, n_coef)), + }, + all_coef=np.random.normal(size=(n_rep, n_coef)), + ) + msg = ( + r"dml_long does not contain the necessary sensitivity elements\. " + r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key sigma2\." + ) with pytest.raises(ValueError, match=msg): _ = gain_statistics(dml_incorrect, dml_correct) - msg = (r"dml_short does not contain the necessary sensitivity elements\. " - r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key sigma2\.") + msg = ( + r"dml_short does not contain the necessary sensitivity elements\. " + r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key sigma2\." + ) with pytest.raises(ValueError, match=msg): _ = gain_statistics(dml_correct, dml_incorrect) dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - 'nu2': np.random.normal(size=(n_obs + 1, n_rep, n_coef)) - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) - msg = (r"dml_long does not contain the necessary sensitivity elements\. " - r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key nu2\.") + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), + "nu2": np.random.normal(size=(n_obs + 1, n_rep, n_coef)), + }, + all_coef=np.random.normal(size=(n_rep, n_coef)), + ) + msg = ( + r"dml_long does not contain the necessary sensitivity elements\. " + r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key nu2\." + ) with pytest.raises(ValueError, match=msg): _ = gain_statistics(dml_incorrect, dml_correct) - msg = (r"dml_short does not contain the necessary sensitivity elements\. " - r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key nu2\.") + msg = ( + r"dml_short does not contain the necessary sensitivity elements\. " + r"Expected 3 dimensions of shape \(1, n_coef, n_rep\) for key nu2\." + ) with pytest.raises(ValueError, match=msg): _ = gain_statistics(dml_correct, dml_incorrect) # conflicting shape for keys dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep + 1, n_coef)), - 'nu2': np.random.normal(size=(n_obs, n_rep, n_coef)) - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs, n_rep + 1, n_coef)), + "nu2": np.random.normal(size=(n_obs, n_rep, n_coef)), + }, + all_coef=np.random.normal(size=(n_rep, n_coef)), + ) msg = r"dml_long and dml_short do not contain the same shape of sensitivity elements\. " msg += r"Shapes of sigma2 are: \(1, 4, 5\) and \(1, 3, 5\)" with pytest.raises(ValueError, match=msg): @@ -138,12 +139,12 @@ def test_doubleml_exception_data(): _ = gain_statistics(dml_correct, dml_incorrect) dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - 'nu2': np.random.normal(size=(n_obs, n_rep + 1, n_coef)) - }, - all_coef=np.random.normal(size=(n_rep, n_coef)) - ) + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), + "nu2": np.random.normal(size=(n_obs, n_rep + 1, n_coef)), + }, + all_coef=np.random.normal(size=(n_rep, n_coef)), + ) msg = r"dml_long and dml_short do not contain the same shape of sensitivity elements\. " msg += r"Shapes of nu2 are: \(1, 4, 5\) and \(1, 3, 5\)" with pytest.raises(ValueError, match=msg): @@ -155,12 +156,12 @@ def test_doubleml_exception_data(): # incorrect type for all_coef dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - 'nu2': np.random.normal(size=(n_obs, n_rep, n_coef)) - }, - all_coef={} - ) + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), + "nu2": np.random.normal(size=(n_obs, n_rep, n_coef)), + }, + all_coef={}, + ) msg = r"dml_long\.all_coef does not contain the necessary coefficients\. Expected numpy\.ndarray\." with pytest.raises(TypeError, match=msg): _ = gain_statistics(dml_incorrect, dml_correct) @@ -170,12 +171,12 @@ def test_doubleml_exception_data(): # incorrect shape for all_coef dml_incorrect = test_dml_class( - sensitivity_elements={ - 'sigma2': np.random.normal(size=(n_obs, n_rep, n_coef)), - 'nu2': np.random.normal(size=(n_obs, n_rep, n_coef)) - }, - all_coef=np.random.normal(size=(n_rep, n_coef + 1)) - ) + sensitivity_elements={ + "sigma2": np.random.normal(size=(n_obs, n_rep, n_coef)), + "nu2": np.random.normal(size=(n_obs, n_rep, n_coef)), + }, + all_coef=np.random.normal(size=(n_rep, n_coef + 1)), + ) msg = r"dml_long\.all_coef does not contain the necessary coefficients\. Expected shape: \(3, 5\)" with pytest.raises(ValueError, match=msg): _ = gain_statistics(dml_incorrect, dml_correct) diff --git a/doubleml/utils/tests/test_global_learners.py b/doubleml/utils/tests/test_global_learners.py index ee3aedb94..5c8844dd8 100644 --- a/doubleml/utils/tests/test_global_learners.py +++ b/doubleml/utils/tests/test_global_learners.py @@ -7,16 +7,17 @@ from doubleml.utils import GlobalClassifier, GlobalRegressor -@pytest.fixture(scope='module', - params=[LinearRegression(), - RandomForestRegressor(n_estimators=10, max_depth=2, random_state=42)]) +@pytest.fixture( + scope="module", params=[LinearRegression(), RandomForestRegressor(n_estimators=10, max_depth=2, random_state=42)] +) def regressor(request): return request.param -@pytest.fixture(scope='module', - params=[LogisticRegression(random_state=42), - RandomForestClassifier(n_estimators=10, max_depth=2, random_state=42)]) +@pytest.fixture( + scope="module", + params=[LogisticRegression(random_state=42), RandomForestClassifier(n_estimators=10, max_depth=2, random_state=42)], +) def classifier(request): return request.param @@ -77,7 +78,7 @@ def gl_fixture(regressor, classifier): "unweighted_clas_pred": unweighted_clas_pred, "global_clas_pred_proba": global_clas_pred_proba, "weighted_clas_pred_proba": weighted_clas_pred_proba, - "unweighted_clas_pred_proba": unweighted_clas_pred_proba + "unweighted_clas_pred_proba": unweighted_clas_pred_proba, } return result_dict diff --git a/doubleml/utils/tests/test_policytree.py b/doubleml/utils/tests/test_policytree.py index f07906d2f..28c2ab7c2 100644 --- a/doubleml/utils/tests/test_policytree.py +++ b/doubleml/utils/tests/test_policytree.py @@ -11,18 +11,17 @@ from ._utils_pt_manual import fit_policytree -@pytest.fixture(scope='module', - params=[1, 2, 3]) +@pytest.fixture(scope="module", params=[1, 2, 3]) def depth(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dml_policytree_fixture(depth): n = 50 np.random.seed(42) random_x_var = pd.DataFrame(np.random.normal(0, 1, size=(n, 3))) - random_signal = np.random.normal(0, 1, size=(n, )) + random_signal = np.random.normal(0, 1, size=(n,)) policy_tree = dml.DoubleMLPolicyTree(random_signal, random_x_var, depth) @@ -32,61 +31,65 @@ def dml_policytree_fixture(depth): np.random.seed(42) policy_tree_manual = fit_policytree(random_signal, random_x_var, depth) - res_dict = {'tree': policy_tree.policy_tree.tree_, - 'tree_manual': policy_tree_manual.tree_, - 'features': policy_tree.features, - 'signal': policy_tree.orth_signal, - 'policytree_model': policy_tree, - 'unfitted_policytree_model': policy_tree_obj} + res_dict = { + "tree": policy_tree.policy_tree.tree_, + "tree_manual": policy_tree_manual.tree_, + "features": policy_tree.features, + "signal": policy_tree.orth_signal, + "policytree_model": policy_tree, + "unfitted_policytree_model": policy_tree_obj, + } return res_dict @pytest.mark.ci def test_dml_policytree_treshold(dml_policytree_fixture): - assert np.allclose(dml_policytree_fixture['tree'].threshold, - dml_policytree_fixture['tree_manual'].threshold, - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_policytree_fixture["tree"].threshold, dml_policytree_fixture["tree_manual"].threshold, rtol=1e-9, atol=1e-4 + ) @pytest.mark.ci def test_dml_policytree_children(dml_policytree_fixture): - assert np.allclose(dml_policytree_fixture['tree'].children_left, - dml_policytree_fixture['tree_manual'].children_left, - rtol=1e-9, atol=1e-4) - assert np.allclose(dml_policytree_fixture['tree'].children_right, - dml_policytree_fixture['tree_manual'].children_right, - rtol=1e-9, atol=1e-4) + assert np.allclose( + dml_policytree_fixture["tree"].children_left, dml_policytree_fixture["tree_manual"].children_left, rtol=1e-9, atol=1e-4 + ) + assert np.allclose( + dml_policytree_fixture["tree"].children_right, + dml_policytree_fixture["tree_manual"].children_right, + rtol=1e-9, + atol=1e-4, + ) @pytest.mark.ci def test_dml_policytree_return_types(dml_policytree_fixture): - assert isinstance(dml_policytree_fixture['policytree_model'].__str__(), str) - assert isinstance(dml_policytree_fixture['policytree_model'].summary, pd.DataFrame) - assert isinstance(dml_policytree_fixture['policytree_model'].policy_tree, DecisionTreeClassifier) + assert isinstance(dml_policytree_fixture["policytree_model"].__str__(), str) + assert isinstance(dml_policytree_fixture["policytree_model"].summary, pd.DataFrame) + assert isinstance(dml_policytree_fixture["policytree_model"].policy_tree, DecisionTreeClassifier) @pytest.mark.ci def test_doubleml_exception_policytree(): - random_features = pd.DataFrame(np.random.normal(0, 1, size=(2, 3)), columns=['a', 'b', 'c']) + random_features = pd.DataFrame(np.random.normal(0, 1, size=(2, 3)), columns=["a", "b", "c"]) signal = np.array([1, 2]) msg = "The signal must be of np.ndarray type. Signal of type was passed." with pytest.raises(TypeError, match=msg): dml.DoubleMLPolicyTree(orth_signal=1, features=random_features) - msg = 'The signal must be of one dimensional. Signal of dimensions 2 was passed.' + msg = "The signal must be of one dimensional. Signal of dimensions 2 was passed." with pytest.raises(ValueError, match=msg): dml.DoubleMLPolicyTree(orth_signal=np.array([[1], [2]]), features=random_features) msg = "The features must be of DataFrame type. Features of type was passed." with pytest.raises(TypeError, match=msg): dml.DoubleMLPolicyTree(orth_signal=signal, features=1) - msg = 'Invalid pd.DataFrame: Contains duplicate column names.' + msg = "Invalid pd.DataFrame: Contains duplicate column names." with pytest.raises(ValueError, match=msg): - dml.DoubleMLPolicyTree(orth_signal=signal, features=pd.DataFrame(np.array([[1, 2], [4, 5]]), - columns=['a_1', 'a_1'])) + dml.DoubleMLPolicyTree(orth_signal=signal, features=pd.DataFrame(np.array([[1, 2], [4, 5]]), columns=["a_1", "a_1"])) dml_policytree_predict = dml.DoubleMLPolicyTree(orth_signal=signal, features=random_features) - msg = 'Policy Tree not yet fitted. Call fit before predict.' + msg = "Policy Tree not yet fitted. Call fit before predict." with pytest.raises(NotFittedError, match=msg): dml_policytree_predict.predict(random_features) @@ -94,12 +97,14 @@ def test_doubleml_exception_policytree(): msg = "The features must be of DataFrame type. Features of type was passed." with pytest.raises(TypeError, match=msg): dml_policytree_predict.predict(features=1) - msg = (r'The features must have the keys Index\(\[\'a\', \'b\', \'c\'\], dtype\=\'object\'\). ' - r'Features with keys Index\(\[\'d\'\], dtype=\'object\'\) were passed.') + msg = ( + r"The features must have the keys Index\(\[\'a\', \'b\', \'c\'\], dtype\=\'object\'\). " + r"Features with keys Index\(\[\'d\'\], dtype=\'object\'\) were passed." + ) with pytest.raises(KeyError, match=msg): dml_policytree_predict.predict(features=pd.DataFrame({"d": [3, 4]})) dml_policytree_plot = dml.DoubleMLPolicyTree(orth_signal=signal, features=random_features) - msg = 'Policy Tree not yet fitted. Call fit before plot_tree.' + msg = "Policy Tree not yet fitted. Call fit before plot_tree." with pytest.raises(NotFittedError, match=msg): dml_policytree_plot.plot_tree() diff --git a/doubleml/utils/tests/test_var_est_and_aggregation.py b/doubleml/utils/tests/test_var_est_and_aggregation.py index 8273423df..22abe6954 100644 --- a/doubleml/utils/tests/test_var_est_and_aggregation.py +++ b/doubleml/utils/tests/test_var_est_and_aggregation.py @@ -4,19 +4,17 @@ from doubleml.utils._estimation import _aggregate_coefs_and_ses, _var_est -@pytest.fixture(scope='module', - params=[1, 3]) +@pytest.fixture(scope="module", params=[1, 3]) def n_rep(request): return request.param -@pytest.fixture(scope='module', - params=[1, 5]) +@pytest.fixture(scope="module", params=[1, 5]) def n_coefs(request): return request.param -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def test_var_est_and_aggr_fixture(n_rep, n_coefs): np.random.seed(42) @@ -34,12 +32,7 @@ def test_var_est_and_aggr_fixture(n_rep, n_coefs): all_thetas[i_coef, i_rep] = np.mean(psi) - var_estimate, var_scaling_factor = _var_est( - psi=psi, - psi_deriv=psi_deriv, - smpls=None, - is_cluster_data=False - ) + var_estimate, var_scaling_factor = _var_est(psi=psi, psi_deriv=psi_deriv, smpls=None, is_cluster_data=False) all_ses[i_coef, i_rep] = np.sqrt(var_estimate) all_var_scaling_factors[i_coef, i_rep] = var_scaling_factor @@ -48,8 +41,9 @@ def test_var_est_and_aggr_fixture(n_rep, n_coefs): for i_coef in range(n_coefs): for i_rep in range(n_rep): theta_deviation = np.square(all_thetas[i_coef, i_rep] - expected_theta[i_coef]) - expected_all_var[i_coef, i_rep] = np.square(all_ses[i_coef, i_rep]) + \ - np.divide(theta_deviation, all_var_scaling_factors[i_coef, i_rep]) + expected_all_var[i_coef, i_rep] = np.square(all_ses[i_coef, i_rep]) + np.divide( + theta_deviation, all_var_scaling_factors[i_coef, i_rep] + ) expected_se = np.sqrt(np.median(expected_all_var, axis=1)) @@ -68,36 +62,24 @@ def test_var_est_and_aggr_fixture(n_rep, n_coefs): ) result_dict = { - 'theta': theta, - 'se': se, - 'theta_2': theta_2, - 'se_2': se_2, - 'expected_theta': expected_theta, - 'expected_se': expected_se, - 'all_var_scaling_factors': all_var_scaling_factors, + "theta": theta, + "se": se, + "theta_2": theta_2, + "se_2": se_2, + "expected_theta": expected_theta, + "expected_se": expected_se, + "all_var_scaling_factors": all_var_scaling_factors, } return result_dict @pytest.mark.ci def test_aggregate_theta(test_var_est_and_aggr_fixture): - assert np.allclose( - test_var_est_and_aggr_fixture['theta'], - test_var_est_and_aggr_fixture['expected_theta'] - ) - assert np.allclose( - test_var_est_and_aggr_fixture['theta_2'], - test_var_est_and_aggr_fixture['expected_theta'] - ) + assert np.allclose(test_var_est_and_aggr_fixture["theta"], test_var_est_and_aggr_fixture["expected_theta"]) + assert np.allclose(test_var_est_and_aggr_fixture["theta_2"], test_var_est_and_aggr_fixture["expected_theta"]) @pytest.mark.ci def test_aggregate_se(test_var_est_and_aggr_fixture): - assert np.allclose( - test_var_est_and_aggr_fixture['se'], - test_var_est_and_aggr_fixture['expected_se'] - ) - assert np.allclose( - test_var_est_and_aggr_fixture['se_2'], - test_var_est_and_aggr_fixture['expected_se'] - ) + assert np.allclose(test_var_est_and_aggr_fixture["se"], test_var_est_and_aggr_fixture["expected_se"]) + assert np.allclose(test_var_est_and_aggr_fixture["se_2"], test_var_est_and_aggr_fixture["expected_se"]) From f984773c36d5ae092bf7f0febeda3544e0994906 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:44:01 +0100 Subject: [PATCH 16/28] ruff doubleml --- doubleml/__init__.py | 22 ++++++++++------------ doubleml/datasets.py | 11 +++++------ doubleml/double_ml.py | 17 +++++++---------- doubleml/double_ml_data.py | 10 +++++----- doubleml/double_ml_framework.py | 23 +++++++++++++++-------- doubleml/double_ml_score_mixins.py | 8 +++----- 6 files changed, 45 insertions(+), 46 deletions(-) diff --git a/doubleml/__init__.py b/doubleml/__init__.py index c97bddf79..025d0739c 100644 --- a/doubleml/__init__.py +++ b/doubleml/__init__.py @@ -1,22 +1,20 @@ import importlib.metadata -from .double_ml_framework import concat -from .double_ml_framework import DoubleMLFramework -from .plm.plr import DoubleMLPLR -from .plm.pliv import DoubleMLPLIV -from .irm.irm import DoubleMLIRM +from .did.did import DoubleMLDID +from .did.did_cs import DoubleMLDIDCS +from .double_ml_data import DoubleMLClusterData, DoubleMLData +from .double_ml_framework import DoubleMLFramework, concat from .irm.apo import DoubleMLAPO from .irm.apos import DoubleMLAPOS +from .irm.cvar import DoubleMLCVAR from .irm.iivm import DoubleMLIIVM -from .double_ml_data import DoubleMLData, DoubleMLClusterData -from .did.did import DoubleMLDID -from .did.did_cs import DoubleMLDIDCS -from .irm.qte import DoubleMLQTE -from .irm.pq import DoubleMLPQ +from .irm.irm import DoubleMLIRM from .irm.lpq import DoubleMLLPQ -from .irm.cvar import DoubleMLCVAR +from .irm.pq import DoubleMLPQ +from .irm.qte import DoubleMLQTE from .irm.ssm import DoubleMLSSM - +from .plm.pliv import DoubleMLPLIV +from .plm.plr import DoubleMLPLR from .utils.blp import DoubleMLBLP from .utils.policytree import DoubleMLPolicyTree diff --git a/doubleml/datasets.py b/doubleml/datasets.py index 74eceb0fc..324290287 100644 --- a/doubleml/datasets.py +++ b/doubleml/datasets.py @@ -1,14 +1,13 @@ -import pandas as pd -import numpy as np import warnings +import numpy as np +import pandas as pd from scipy.linalg import toeplitz from scipy.optimize import minimize_scalar - -from sklearn.preprocessing import PolynomialFeatures, OneHotEncoder from sklearn.datasets import make_spd_matrix +from sklearn.preprocessing import OneHotEncoder, PolynomialFeatures -from .double_ml_data import DoubleMLData, DoubleMLClusterData +from .double_ml_data import DoubleMLClusterData, DoubleMLData _array_alias = ['array', 'np.ndarray', 'np.array', np.ndarray] _data_frame_alias = ['DataFrame', 'pd.DataFrame', pd.DataFrame] @@ -82,7 +81,7 @@ def fetch_bonus(return_type='DoubleMLData', polynomial_features=False): doi:`10.1111/ectj.12097 `_. """ url = 'https://raw.githubusercontent.com/VC2015/DMLonGitHub/master/penn_jae.dat' - raw_data = pd.read_csv(url, sep='\s+') + raw_data = pd.read_csv(url, sep=r'\s+') ind = (raw_data['tg'] == 0) | (raw_data['tg'] == 4) data = raw_data.copy()[ind] diff --git a/doubleml/double_ml.py b/doubleml/double_ml.py index 71f8b4418..afab3f56c 100644 --- a/doubleml/double_ml.py +++ b/doubleml/double_ml.py @@ -1,21 +1,18 @@ -import numpy as np -import pandas as pd -import warnings import copy +import warnings +from abc import ABC, abstractmethod -from sklearn.base import is_regressor, is_classifier - +import numpy as np +import pandas as pd from scipy.stats import norm - -from abc import ABC, abstractmethod +from sklearn.base import is_classifier, is_regressor from .double_ml_data import DoubleMLBaseData, DoubleMLClusterData from .double_ml_framework import DoubleMLFramework - -from .utils.resampling import DoubleMLResampling, DoubleMLClusterResampling -from .utils._estimation import _rmse, _aggregate_coefs_and_ses, _var_est, _set_external_predictions from .utils._checks import _check_external_predictions, _check_sample_splitting +from .utils._estimation import _aggregate_coefs_and_ses, _rmse, _set_external_predictions, _var_est from .utils.gain_statistics import gain_statistics +from .utils.resampling import DoubleMLClusterResampling, DoubleMLResampling _implemented_data_backends = ['DoubleMLData', 'DoubleMLClusterData'] diff --git a/doubleml/double_ml_data.py b/doubleml/double_ml_data.py index fdee739dd..95e067fcb 100644 --- a/doubleml/double_ml_data.py +++ b/doubleml/double_ml_data.py @@ -1,14 +1,14 @@ -import numpy as np -import pandas as pd import io - from abc import ABC, abstractmethod -from sklearn.utils.validation import check_array, column_or_1d, check_consistent_length +import numpy as np +import pandas as pd from sklearn.utils import assert_all_finite from sklearn.utils.multiclass import type_of_target -from .utils._estimation import _assure_2d_array +from sklearn.utils.validation import check_array, check_consistent_length, column_or_1d + from .utils._checks import _check_set +from .utils._estimation import _assure_2d_array class DoubleMLBaseData(ABC): diff --git a/doubleml/double_ml_framework.py b/doubleml/double_ml_framework.py index b70a16230..d4af13c60 100644 --- a/doubleml/double_ml_framework.py +++ b/doubleml/double_ml_framework.py @@ -1,15 +1,22 @@ -import numpy as np -import pandas as pd import copy -from scipy.stats import norm +import numpy as np +import pandas as pd from scipy.optimize import minimize_scalar +from scipy.stats import norm from statsmodels.stats.multitest import multipletests -from .utils._estimation import _draw_weights, _aggregate_coefs_and_ses, _var_est -from .utils._checks import _check_bootstrap, _check_framework_compatibility, _check_in_zero_one, \ - _check_float, _check_integer, _check_bool, _check_benchmarks +from .utils._checks import ( + _check_benchmarks, + _check_bool, + _check_bootstrap, + _check_float, + _check_framework_compatibility, + _check_in_zero_one, + _check_integer, +) from .utils._descriptive import generate_summary +from .utils._estimation import _aggregate_coefs_and_ses, _draw_weights, _var_est from .utils._plots import _sensitivity_contour_plot @@ -941,7 +948,7 @@ def _check_and_set_cluster_data(self, doubleml_dict): self._is_cluster_data = doubleml_dict['is_cluster_data'] if self._is_cluster_data: - if not ("cluster_dict" in doubleml_dict.keys()): + if "cluster_dict" not in doubleml_dict.keys(): raise ValueError('If is_cluster_data is True, cluster_dict must be provided.') if not isinstance(doubleml_dict['cluster_dict'], dict): @@ -957,7 +964,7 @@ def _check_and_set_cluster_data(self, doubleml_dict): return def _check_and_set_sensitivity_elements(self, doubleml_dict): - if not ("sensitivity_elements" in doubleml_dict.keys()): + if "sensitivity_elements" not in doubleml_dict.keys(): sensitivity_implemented = False sensitivity_elements = None diff --git a/doubleml/double_ml_score_mixins.py b/doubleml/double_ml_score_mixins.py index b6a003221..7ac4cbf42 100644 --- a/doubleml/double_ml_score_mixins.py +++ b/doubleml/double_ml_score_mixins.py @@ -1,13 +1,11 @@ import copy - -import numpy as np - import warnings +from abc import abstractmethod +import numpy as np from scipy.optimize import fmin_l_bfgs_b, root_scalar -from .utils._estimation import _get_bracket_guess -from abc import abstractmethod +from .utils._estimation import _get_bracket_guess class LinearScoreMixin: From 68683ac211d4b3ab2305c2c1c6f0d77dffb2351b Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:44:58 +0100 Subject: [PATCH 17/28] format doubleml --- doubleml/__init__.py | 40 +- doubleml/datasets.py | 725 ++++++++++++++++++----------- doubleml/double_ml.py | 634 ++++++++++++++----------- doubleml/double_ml_data.py | 449 ++++++++++-------- doubleml/double_ml_framework.py | 640 +++++++++++++------------ doubleml/double_ml_score_mixins.py | 91 ++-- 6 files changed, 1457 insertions(+), 1122 deletions(-) diff --git a/doubleml/__init__.py b/doubleml/__init__.py index 025d0739c..a86735c89 100644 --- a/doubleml/__init__.py +++ b/doubleml/__init__.py @@ -19,25 +19,25 @@ from .utils.policytree import DoubleMLPolicyTree __all__ = [ - 'concat', - 'DoubleMLFramework', - 'DoubleMLPLR', - 'DoubleMLPLIV', - 'DoubleMLIRM', - 'DoubleMLAPO', - 'DoubleMLAPOS', - 'DoubleMLIIVM', - 'DoubleMLData', - 'DoubleMLClusterData', - 'DoubleMLDID', - 'DoubleMLDIDCS', - 'DoubleMLPQ', - 'DoubleMLQTE', - 'DoubleMLLPQ', - 'DoubleMLCVAR', - 'DoubleMLBLP', - 'DoubleMLPolicyTree', - 'DoubleMLSSM' + "concat", + "DoubleMLFramework", + "DoubleMLPLR", + "DoubleMLPLIV", + "DoubleMLIRM", + "DoubleMLAPO", + "DoubleMLAPOS", + "DoubleMLIIVM", + "DoubleMLData", + "DoubleMLClusterData", + "DoubleMLDID", + "DoubleMLDIDCS", + "DoubleMLPQ", + "DoubleMLQTE", + "DoubleMLLPQ", + "DoubleMLCVAR", + "DoubleMLBLP", + "DoubleMLPolicyTree", + "DoubleMLSSM", ] -__version__ = importlib.metadata.version('doubleml') +__version__ = importlib.metadata.version("doubleml") diff --git a/doubleml/datasets.py b/doubleml/datasets.py index 324290287..909db74a4 100644 --- a/doubleml/datasets.py +++ b/doubleml/datasets.py @@ -9,13 +9,13 @@ from .double_ml_data import DoubleMLClusterData, DoubleMLData -_array_alias = ['array', 'np.ndarray', 'np.array', np.ndarray] -_data_frame_alias = ['DataFrame', 'pd.DataFrame', pd.DataFrame] -_dml_data_alias = ['DoubleMLData', DoubleMLData] -_dml_cluster_data_alias = ['DoubleMLClusterData', DoubleMLClusterData] +_array_alias = ["array", "np.ndarray", "np.array", np.ndarray] +_data_frame_alias = ["DataFrame", "pd.DataFrame", pd.DataFrame] +_dml_data_alias = ["DoubleMLData", DoubleMLData] +_dml_cluster_data_alias = ["DoubleMLClusterData", DoubleMLClusterData] -def fetch_401K(return_type='DoubleMLData', polynomial_features=False): +def fetch_401K(return_type="DoubleMLData", polynomial_features=False): """ Data set on financial wealth and 401(k) plan participation. @@ -37,17 +37,17 @@ def fetch_401K(return_type='DoubleMLData', polynomial_features=False): Double/debiased machine learning for treatment and structural parameters. The Econometrics Journal, 21: C1-C68. doi:`10.1111/ectj.12097 `_. """ - url = 'https://github.com/VC2015/DMLonGitHub/raw/master/sipp1991.dta' + url = "https://github.com/VC2015/DMLonGitHub/raw/master/sipp1991.dta" raw_data = pd.read_stata(url) - y_col = 'net_tfa' - d_cols = ['e401'] - x_cols = ['age', 'inc', 'educ', 'fsize', 'marr', 'twoearn', 'db', 'pira', 'hown'] + y_col = "net_tfa" + d_cols = ["e401"] + x_cols = ["age", "inc", "educ", "fsize", "marr", "twoearn", "db", "pira", "hown"] data = raw_data.copy() if polynomial_features: - raise NotImplementedError('polynomial_features os not implemented yet for fetch_401K.') + raise NotImplementedError("polynomial_features os not implemented yet for fetch_401K.") if return_type in _data_frame_alias + _dml_data_alias: if return_type in _data_frame_alias: @@ -55,10 +55,10 @@ def fetch_401K(return_type='DoubleMLData', polynomial_features=False): else: return DoubleMLData(data, y_col, d_cols, x_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def fetch_bonus(return_type='DoubleMLData', polynomial_features=False): +def fetch_bonus(return_type="DoubleMLData", polynomial_features=False): """ Data set on the Pennsylvania Reemployment Bonus experiment. @@ -80,27 +80,40 @@ def fetch_bonus(return_type='DoubleMLData', polynomial_features=False): Double/debiased machine learning for treatment and structural parameters. The Econometrics Journal, 21: C1-C68. doi:`10.1111/ectj.12097 `_. """ - url = 'https://raw.githubusercontent.com/VC2015/DMLonGitHub/master/penn_jae.dat' - raw_data = pd.read_csv(url, sep=r'\s+') + url = "https://raw.githubusercontent.com/VC2015/DMLonGitHub/master/penn_jae.dat" + raw_data = pd.read_csv(url, sep=r"\s+") - ind = (raw_data['tg'] == 0) | (raw_data['tg'] == 4) + ind = (raw_data["tg"] == 0) | (raw_data["tg"] == 4) data = raw_data.copy()[ind] data.reset_index(inplace=True) - data['tg'] = data['tg'].replace(4, 1) - data['inuidur1'] = np.log(data['inuidur1']) + data["tg"] = data["tg"].replace(4, 1) + data["inuidur1"] = np.log(data["inuidur1"]) # variable dep as factor (dummy encoding) - dummy_enc = OneHotEncoder(drop='first', categories='auto').fit(data.loc[:, ['dep']]) - xx = dummy_enc.transform(data.loc[:, ['dep']]).toarray() - data['dep1'] = xx[:, 0] - data['dep2'] = xx[:, 1] - - y_col = 'inuidur1' - d_cols = ['tg'] - x_cols = ['female', 'black', 'othrace', - 'dep1', 'dep2', - 'q2', 'q3', 'q4', 'q5', 'q6', - 'agelt35', 'agegt54', 'durable', 'lusd', 'husd'] + dummy_enc = OneHotEncoder(drop="first", categories="auto").fit(data.loc[:, ["dep"]]) + xx = dummy_enc.transform(data.loc[:, ["dep"]]).toarray() + data["dep1"] = xx[:, 0] + data["dep2"] = xx[:, 1] + + y_col = "inuidur1" + d_cols = ["tg"] + x_cols = [ + "female", + "black", + "othrace", + "dep1", + "dep2", + "q2", + "q3", + "q4", + "q5", + "q6", + "agelt35", + "agegt54", + "durable", + "lusd", + "husd", + ] if polynomial_features: poly = PolynomialFeatures(2, include_bias=False) @@ -108,8 +121,7 @@ def fetch_bonus(return_type='DoubleMLData', polynomial_features=False): x_cols = list(poly.get_feature_names_out(x_cols)) data_transf = pd.DataFrame(data_transf, columns=x_cols) - data = pd.concat((data[[y_col] + d_cols], data_transf), - axis=1, sort=False) + data = pd.concat((data[[y_col] + d_cols], data_transf), axis=1, sort=False) if return_type in _data_frame_alias + _dml_data_alias: if return_type in _data_frame_alias: @@ -117,18 +129,18 @@ def fetch_bonus(return_type='DoubleMLData', polynomial_features=False): else: return DoubleMLData(data, y_col, d_cols, x_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") def _g(x): return np.power(np.sin(x), 2) -def _m(x, nu=0., gamma=1.): - return 0.5/np.pi*(np.sinh(gamma))/(np.cosh(gamma)-np.cos(x-nu)) +def _m(x, nu=0.0, gamma=1.0): + return 0.5 / np.pi * (np.sinh(gamma)) / (np.cosh(gamma) - np.cos(x - nu)) -def make_plr_CCDDHNR2018(n_obs=500, dim_x=20, alpha=0.5, return_type='DoubleMLData', **kwargs): +def make_plr_CCDDHNR2018(n_obs=500, dim_x=20, alpha=0.5, return_type="DoubleMLData", **kwargs): """ Generates data from a partially linear regression model used in Chernozhukov et al. (2018) for Figure 1. The data generating process is defined as @@ -174,37 +186,59 @@ def make_plr_CCDDHNR2018(n_obs=500, dim_x=20, alpha=0.5, return_type='DoubleMLDa Double/debiased machine learning for treatment and structural parameters. The Econometrics Journal, 21: C1-C68. doi:`10.1111/ectj.12097 `_. """ - a_0 = kwargs.get('a_0', 1.) - a_1 = kwargs.get('a_1', 0.25) - s_1 = kwargs.get('s_1', 1.) + a_0 = kwargs.get("a_0", 1.0) + a_1 = kwargs.get("a_1", 0.25) + s_1 = kwargs.get("s_1", 1.0) - b_0 = kwargs.get('b_0', 1.) - b_1 = kwargs.get('b_1', 0.25) - s_2 = kwargs.get('s_2', 1.) + b_0 = kwargs.get("b_0", 1.0) + b_1 = kwargs.get("b_1", 0.25) + s_2 = kwargs.get("s_2", 1.0) cov_mat = toeplitz([np.power(0.7, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) - d = a_0 * x[:, 0] + a_1 * np.divide(np.exp(x[:, 2]), 1 + np.exp(x[:, 2])) \ - + s_1 * np.random.standard_normal(size=[n_obs, ]) - y = alpha * d + b_0 * np.divide(np.exp(x[:, 0]), 1 + np.exp(x[:, 0])) \ - + b_1 * x[:, 2] + s_2 * np.random.standard_normal(size=[n_obs, ]) + d = ( + a_0 * x[:, 0] + + a_1 * np.divide(np.exp(x[:, 2]), 1 + np.exp(x[:, 2])) + + s_1 + * np.random.standard_normal( + size=[ + n_obs, + ] + ) + ) + y = ( + alpha * d + + b_0 * np.divide(np.exp(x[:, 0]), 1 + np.exp(x[:, 0])) + + b_1 * x[:, 2] + + s_2 + * np.random.standard_normal( + size=[ + n_obs, + ] + ) + ) if return_type in _array_alias: return x, y, d elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=x_cols + ['y', 'd']) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((x, y, d)), columns=x_cols + ["y", "d"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', x_cols) + return DoubleMLData(data, "y", "d", x_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def make_plr_turrell2018(n_obs=100, dim_x=20, theta=0.5, return_type='DoubleMLData', **kwargs): +def make_plr_turrell2018(n_obs=100, dim_x=20, theta=0.5, return_type="DoubleMLData", **kwargs): """ Generates data from a partially linear regression model used in a blog article by Turrell (2018). The data generating process is defined as @@ -250,33 +284,50 @@ def make_plr_turrell2018(n_obs=100, dim_x=20, theta=0.5, return_type='DoubleMLDa science, coding and data. `https://aeturrell.com/blog/posts/econometrics-in-python-parti-ml/ `_. """ - nu = kwargs.get('nu', 0.) - gamma = kwargs.get('gamma', 1.) + nu = kwargs.get("nu", 0.0) + gamma = kwargs.get("gamma", 1.0) b = [1 / k for k in range(1, dim_x + 1)] sigma = make_spd_matrix(dim_x) - x = np.random.multivariate_normal(np.zeros(dim_x), sigma, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + sigma, + size=[ + n_obs, + ], + ) G = _g(np.dot(x, b)) M = _m(np.dot(x, b), nu=nu, gamma=gamma) - d = M + np.random.standard_normal(size=[n_obs, ]) - y = np.dot(theta, d) + G + np.random.standard_normal(size=[n_obs, ]) + d = M + np.random.standard_normal( + size=[ + n_obs, + ] + ) + y = ( + np.dot(theta, d) + + G + + np.random.standard_normal( + size=[ + n_obs, + ] + ) + ) if return_type in _array_alias: return x, y, d elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=x_cols + ['y', 'd']) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((x, y, d)), columns=x_cols + ["y", "d"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', x_cols) + return DoubleMLData(data, "y", "d", x_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def make_irm_data(n_obs=500, dim_x=20, theta=0, R2_d=0.5, R2_y=0.5, return_type='DoubleMLData'): +def make_irm_data(n_obs=500, dim_x=20, theta=0, R2_d=0.5, R2_y=0.5, return_type="DoubleMLData"): """ Generates data from a interactive regression (IRM) model. The data generating process is defined as @@ -326,37 +377,50 @@ def make_irm_data(n_obs=500, dim_x=20, theta=0, R2_d=0.5, R2_y=0.5, return_type= High‐Dimensional Data. Econometrica, 85: 233-298. """ # inspired by https://onlinelibrary.wiley.com/doi/abs/10.3982/ECTA12723, see suplement - v = np.random.uniform(size=[n_obs, ]) - zeta = np.random.standard_normal(size=[n_obs, ]) + v = np.random.uniform( + size=[ + n_obs, + ] + ) + zeta = np.random.standard_normal( + size=[ + n_obs, + ] + ) cov_mat = toeplitz([np.power(0.5, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) beta = [1 / (k**2) for k in range(1, dim_x + 1)] b_sigma_b = np.dot(np.dot(cov_mat, beta), beta) - c_y = np.sqrt(R2_y/((1-R2_y) * b_sigma_b)) - c_d = np.sqrt(np.pi**2 / 3. * R2_d/((1-R2_d) * b_sigma_b)) + c_y = np.sqrt(R2_y / ((1 - R2_y) * b_sigma_b)) + c_d = np.sqrt(np.pi**2 / 3.0 * R2_d / ((1 - R2_d) * b_sigma_b)) xx = np.exp(np.dot(x, np.multiply(beta, c_d))) - d = 1. * ((xx/(1+xx)) > v) + d = 1.0 * ((xx / (1 + xx)) > v) y = d * theta + d * np.dot(x, np.multiply(beta, c_y)) + zeta if return_type in _array_alias: return x, y, d elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=x_cols + ['y', 'd']) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((x, y, d)), columns=x_cols + ["y", "d"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', x_cols) + return DoubleMLData(data, "y", "d", x_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def make_iivm_data(n_obs=500, dim_x=20, theta=1., alpha_x=0.2, return_type='DoubleMLData'): +def make_iivm_data(n_obs=500, dim_x=20, theta=1.0, alpha_x=0.2, return_type="DoubleMLData"): """ Generates data from a interactive IV regression (IIVM) model. The data generating process is defined as @@ -404,64 +468,100 @@ def make_iivm_data(n_obs=500, dim_x=20, theta=1., alpha_x=0.2, return_type='Doub Paper No. 13-2020. Available at SSRN: http://dx.doi.org/10.2139/ssrn.3619201. """ # inspired by https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3619201 - xx = np.random.multivariate_normal(np.zeros(2), - np.array([[1., 0.3], [0.3, 1.]]), - size=[n_obs, ]) + xx = np.random.multivariate_normal( + np.zeros(2), + np.array([[1.0, 0.3], [0.3, 1.0]]), + size=[ + n_obs, + ], + ) u = xx[:, 0] v = xx[:, 1] cov_mat = toeplitz([np.power(0.5, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) beta = [1 / (k**2) for k in range(1, dim_x + 1)] - z = np.random.binomial(p=0.5, n=1, size=[n_obs, ]) - d = 1. * (alpha_x * z + v > 0) + z = np.random.binomial( + p=0.5, + n=1, + size=[ + n_obs, + ], + ) + d = 1.0 * (alpha_x * z + v > 0) y = d * theta + np.dot(x, beta) + u if return_type in _array_alias: return x, y, d, z elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((x, y, d, z)), - columns=x_cols + ['y', 'd', 'z']) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((x, y, d, z)), columns=x_cols + ["y", "d", "z"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', x_cols, 'z') + return DoubleMLData(data, "y", "d", x_cols, "z") else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def _make_pliv_data(n_obs=100, dim_x=20, theta=0.5, gamma_z=0.4, return_type='DoubleMLData'): - b = [1/k for k in range(1, dim_x+1)] +def _make_pliv_data(n_obs=100, dim_x=20, theta=0.5, gamma_z=0.4, return_type="DoubleMLData"): + b = [1 / k for k in range(1, dim_x + 1)] sigma = make_spd_matrix(dim_x) - x = np.random.multivariate_normal(np.zeros(dim_x), sigma, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + sigma, + size=[ + n_obs, + ], + ) G = _g(np.dot(x, b)) # instrument - z = _m(np.dot(x, b)) + np.random.standard_normal(size=[n_obs, ]) + z = _m(np.dot(x, b)) + np.random.standard_normal( + size=[ + n_obs, + ] + ) # treatment M = _m(gamma_z * z + np.dot(x, b)) - d = M + np.random.standard_normal(size=[n_obs, ]) - y = np.dot(theta, d) + G + np.random.standard_normal(size=[n_obs, ]) + d = M + np.random.standard_normal( + size=[ + n_obs, + ] + ) + y = ( + np.dot(theta, d) + + G + + np.random.standard_normal( + size=[ + n_obs, + ] + ) + ) if return_type in _array_alias: return x, y, d, z elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((x, y, d, z)), - columns=x_cols + ['y', 'd', 'z']) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((x, y, d, z)), columns=x_cols + ["y", "d", "z"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', x_cols, 'z') + return DoubleMLData(data, "y", "d", x_cols, "z") else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def make_pliv_CHS2015(n_obs, alpha=1., dim_x=200, dim_z=150, return_type='DoubleMLData'): +def make_pliv_CHS2015(n_obs, alpha=1.0, dim_x=200, dim_z=150, return_type="DoubleMLData"): """ Generates data from a partially linear IV regression model used in Chernozhukov, Hansen and Spindler (2015). The data generating process is defined as @@ -512,26 +612,38 @@ def make_pliv_CHS2015(n_obs, alpha=1., dim_x=200, dim_z=150, return_type='Double """ assert dim_x >= dim_z # see https://assets.aeaweb.org/asset-server/articles-attachments/aer/app/10505/P2015_1022_app.pdf - xx = np.random.multivariate_normal(np.zeros(2), - np.array([[1., 0.6], [0.6, 1.]]), - size=[n_obs, ]) + xx = np.random.multivariate_normal( + np.zeros(2), + np.array([[1.0, 0.6], [0.6, 1.0]]), + size=[ + n_obs, + ], + ) epsilon = xx[:, 0] u = xx[:, 1] sigma = toeplitz([np.power(0.5, k) for k in range(0, dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), - sigma, - size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + sigma, + size=[ + n_obs, + ], + ) I_z = np.eye(dim_z) - xi = np.random.multivariate_normal(np.zeros(dim_z), - 0.25*I_z, - size=[n_obs, ]) + xi = np.random.multivariate_normal( + np.zeros(dim_z), + 0.25 * I_z, + size=[ + n_obs, + ], + ) beta = [1 / (k**2) for k in range(1, dim_x + 1)] gamma = beta delta = [1 / (k**2) for k in range(1, dim_z + 1)] - Pi = np.hstack((I_z, np.zeros((dim_z, dim_x-dim_z)))) + Pi = np.hstack((I_z, np.zeros((dim_z, dim_x - dim_z)))) z = np.dot(x, np.transpose(Pi)) + xi d = np.dot(x, gamma) + np.dot(z, delta) + u @@ -540,19 +652,18 @@ def make_pliv_CHS2015(n_obs, alpha=1., dim_x=200, dim_z=150, return_type='Double if return_type in _array_alias: return x, y, d, z elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] - z_cols = [f'Z{i + 1}' for i in np.arange(dim_z)] - data = pd.DataFrame(np.column_stack((x, y, d, z)), - columns=x_cols + ['y', 'd'] + z_cols) + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] + z_cols = [f"Z{i + 1}" for i in np.arange(dim_z)] + data = pd.DataFrame(np.column_stack((x, y, d, z)), columns=x_cols + ["y", "d"] + z_cols) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', x_cols, z_cols) + return DoubleMLData(data, "y", "d", x_cols, z_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def make_pliv_multiway_cluster_CKMS2021(N=25, M=25, dim_X=100, theta=1., return_type='DoubleMLClusterData', **kwargs): +def make_pliv_multiway_cluster_CKMS2021(N=25, M=25, dim_X=100, theta=1.0, return_type="DoubleMLClusterData", **kwargs): """ Generates data from a partially linear IV regression model with multiway cluster sample used in Chiang et al. (2021). The data generating process is defined as @@ -630,20 +741,20 @@ def make_pliv_multiway_cluster_CKMS2021(N=25, M=25, dim_X=100, theta=1., return_ arXiv:`1909.03489 `_. """ # additional parameters specifiable via kwargs - pi_10 = kwargs.get('pi_10', 1.0) + pi_10 = kwargs.get("pi_10", 1.0) xx = np.arange(1, dim_X + 1) - zeta_0 = kwargs.get('zeta_0', np.power(0.5, xx)) - pi_20 = kwargs.get('pi_20', np.power(0.5, xx)) - xi_0 = kwargs.get('xi_0', np.power(0.5, xx)) + zeta_0 = kwargs.get("zeta_0", np.power(0.5, xx)) + pi_20 = kwargs.get("pi_20", np.power(0.5, xx)) + xi_0 = kwargs.get("xi_0", np.power(0.5, xx)) - omega_X = kwargs.get('omega_X', np.array([0.25, 0.25])) - omega_epsilon = kwargs.get('omega_epsilon', np.array([0.25, 0.25])) - omega_v = kwargs.get('omega_v', np.array([0.25, 0.25])) - omega_V = kwargs.get('omega_V', np.array([0.25, 0.25])) + omega_X = kwargs.get("omega_X", np.array([0.25, 0.25])) + omega_epsilon = kwargs.get("omega_epsilon", np.array([0.25, 0.25])) + omega_v = kwargs.get("omega_v", np.array([0.25, 0.25])) + omega_V = kwargs.get("omega_V", np.array([0.25, 0.25])) - s_X = kwargs.get('s_X', 0.25) - s_epsilon_v = kwargs.get('s_epsilon_v', 0.25) + s_X = kwargs.get("s_X", 0.25) + s_epsilon_v = kwargs.get("s_epsilon_v", 0.25) # use np.tile() and np.repeat() for repeating vectors in different styles, i.e., # np.tile([v1, v2, v3], 2) [v1, v2, v3, v1, v2, v3] @@ -654,61 +765,98 @@ def make_pliv_multiway_cluster_CKMS2021(N=25, M=25, dim_X=100, theta=1., return_ alpha_V_j = np.tile(np.random.normal(size=M), N) cov_mat = np.array([[1, s_epsilon_v], [s_epsilon_v, 1]]) - alpha_eps_v = np.random.multivariate_normal(np.zeros(2), cov_mat, size=[N * M, ]) + alpha_eps_v = np.random.multivariate_normal( + np.zeros(2), + cov_mat, + size=[ + N * M, + ], + ) alpha_eps = alpha_eps_v[:, 0] alpha_v = alpha_eps_v[:, 1] - alpha_eps_v_i = np.random.multivariate_normal(np.zeros(2), cov_mat, size=[N, ]) + alpha_eps_v_i = np.random.multivariate_normal( + np.zeros(2), + cov_mat, + size=[ + N, + ], + ) alpha_eps_i = np.repeat(alpha_eps_v_i[:, 0], M) alpha_v_i = np.repeat(alpha_eps_v_i[:, 1], M) - alpha_eps_v_j = np.random.multivariate_normal(np.zeros(2), cov_mat, size=[M, ]) + alpha_eps_v_j = np.random.multivariate_normal( + np.zeros(2), + cov_mat, + size=[ + M, + ], + ) alpha_eps_j = np.tile(alpha_eps_v_j[:, 0], N) alpha_v_j = np.tile(alpha_eps_v_j[:, 1], N) cov_mat = toeplitz([np.power(s_X, k) for k in range(dim_X)]) - alpha_X = np.random.multivariate_normal(np.zeros(dim_X), cov_mat, size=[N * M, ]) - alpha_X_i = np.repeat(np.random.multivariate_normal(np.zeros(dim_X), cov_mat, size=[N, ]), - M, axis=0) - alpha_X_j = np.tile(np.random.multivariate_normal(np.zeros(dim_X), cov_mat, size=[M, ]), - (N, 1)) + alpha_X = np.random.multivariate_normal( + np.zeros(dim_X), + cov_mat, + size=[ + N * M, + ], + ) + alpha_X_i = np.repeat( + np.random.multivariate_normal( + np.zeros(dim_X), + cov_mat, + size=[ + N, + ], + ), + M, + axis=0, + ) + alpha_X_j = np.tile( + np.random.multivariate_normal( + np.zeros(dim_X), + cov_mat, + size=[ + M, + ], + ), + (N, 1), + ) # generate variables - x = (1 - omega_X[0] - omega_X[1]) * alpha_X \ - + omega_X[0] * alpha_X_i + omega_X[1] * alpha_X_j + x = (1 - omega_X[0] - omega_X[1]) * alpha_X + omega_X[0] * alpha_X_i + omega_X[1] * alpha_X_j - eps = (1 - omega_epsilon[0] - omega_epsilon[1]) * alpha_eps \ - + omega_epsilon[0] * alpha_eps_i + omega_epsilon[1] * alpha_eps_j + eps = ( + (1 - omega_epsilon[0] - omega_epsilon[1]) * alpha_eps + omega_epsilon[0] * alpha_eps_i + omega_epsilon[1] * alpha_eps_j + ) - v = (1 - omega_v[0] - omega_v[1]) * alpha_v \ - + omega_v[0] * alpha_v_i + omega_v[1] * alpha_v_j + v = (1 - omega_v[0] - omega_v[1]) * alpha_v + omega_v[0] * alpha_v_i + omega_v[1] * alpha_v_j - V = (1 - omega_V[0] - omega_V[1]) * alpha_V \ - + omega_V[0] * alpha_V_i + omega_V[1] * alpha_V_j + V = (1 - omega_V[0] - omega_V[1]) * alpha_V + omega_V[0] * alpha_V_i + omega_V[1] * alpha_V_j z = np.matmul(x, xi_0) + V d = z * pi_10 + np.matmul(x, pi_20) + v y = d * theta + np.matmul(x, zeta_0) + eps - cluster_cols = ['cluster_var_i', 'cluster_var_j'] + cluster_cols = ["cluster_var_i", "cluster_var_j"] cluster_vars = pd.MultiIndex.from_product([range(N), range(M)]).to_frame(name=cluster_cols).reset_index(drop=True) if return_type in _array_alias: return x, y, d, cluster_vars.values, z elif return_type in _data_frame_alias + _dml_cluster_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_X)] - data = pd.concat((cluster_vars, - pd.DataFrame(np.column_stack((x, y, d, z)), columns=x_cols + ['Y', 'D', 'Z'])), - axis=1) + x_cols = [f"X{i + 1}" for i in np.arange(dim_X)] + data = pd.concat((cluster_vars, pd.DataFrame(np.column_stack((x, y, d, z)), columns=x_cols + ["Y", "D", "Z"])), axis=1) if return_type in _data_frame_alias: return data else: - return DoubleMLClusterData(data, 'Y', 'D', cluster_cols, x_cols, 'Z') + return DoubleMLClusterData(data, "Y", "D", cluster_cols, x_cols, "Z") else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") -def make_did_SZ2020(n_obs=500, dgp_type=1, cross_sectional_data=False, return_type='DoubleMLData', **kwargs): +def make_did_SZ2020(n_obs=500, dgp_type=1, cross_sectional_data=False, return_type="DoubleMLData", **kwargs): """ Generates data from a difference-in-differences model used in Sant'Anna and Zhao (2020). The data generating process is defined as follows. For a generic :math:`W=(W_1, W_2, W_3, W_4)^T`, let @@ -794,26 +942,32 @@ def make_did_SZ2020(n_obs=500, dgp_type=1, cross_sectional_data=False, return_ty Doubly robust difference-in-differences estimators. Journal of Econometrics, 219(1), 101-122. doi:`10.1016/j.jeconom.2020.06.003 `_. """ - xi = kwargs.get('xi', 0.75) - c = kwargs.get('c', 0.0) - lambda_t = kwargs.get('lambda_t', 0.5) + xi = kwargs.get("xi", 0.75) + c = kwargs.get("c", 0.0) + lambda_t = kwargs.get("lambda_t", 0.5) def f_reg(w): - res = 210 + 27.4*w[:, 0] + 13.7*(w[:, 1] + w[:, 2] + w[:, 3]) + res = 210 + 27.4 * w[:, 0] + 13.7 * (w[:, 1] + w[:, 2] + w[:, 3]) return res def f_ps(w, xi): - res = xi*(-w[:, 0] + 0.5*w[:, 1] - 0.25*w[:, 2] - 0.1*w[:, 3]) + res = xi * (-w[:, 0] + 0.5 * w[:, 1] - 0.25 * w[:, 2] - 0.1 * w[:, 3]) return res dim_x = 4 cov_mat = toeplitz([np.power(c, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) - z_tilde_1 = np.exp(0.5*x[:, 0]) + z_tilde_1 = np.exp(0.5 * x[:, 0]) z_tilde_2 = 10 + x[:, 1] / (1 + np.exp(x[:, 0])) - z_tilde_3 = (0.6 + x[:, 0]*x[:, 2]/25)**3 - z_tilde_4 = (20 + x[:, 1] + x[:, 3])**2 + z_tilde_3 = (0.6 + x[:, 0] * x[:, 2] / 25) ** 3 + z_tilde_4 = (20 + x[:, 1] + x[:, 3]) ** 2 z_tilde = np.column_stack((z_tilde_1, z_tilde_2, z_tilde_3, z_tilde_4)) z = (z_tilde - np.mean(z_tilde, axis=0)) / np.std(z_tilde, axis=0) @@ -841,7 +995,7 @@ def f_ps(w, xi): features_ps = None features_reg = x else: - raise ValueError('The dgp_type is not valid.') + raise ValueError("The dgp_type is not valid.") # treatment and propensities is_experimental = (dgp_type == 5) or (dgp_type == 6) @@ -854,11 +1008,11 @@ def f_ps(w, xi): d = 1.0 * (p >= u) # potential outcomes - nu = np.random.normal(loc=d*f_reg(features_reg), scale=1, size=n_obs) + nu = np.random.normal(loc=d * f_reg(features_reg), scale=1, size=n_obs) y0 = f_reg(features_reg) + nu + epsilon_0 y1_d0 = 2 * f_reg(features_reg) + nu + epsilon_1[:, 0] y1_d1 = 2 * f_reg(features_reg) + nu + epsilon_1[:, 1] - y1 = d * y1_d1 + (1-d) * y1_d0 + y1 = d * y1_d1 + (1 - d) * y1_d0 if not cross_sectional_data: y = y1 - y0 @@ -866,33 +1020,31 @@ def f_ps(w, xi): if return_type in _array_alias: return z, y, d elif return_type in _data_frame_alias + _dml_data_alias: - z_cols = [f'Z{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((z, y, d)), - columns=z_cols + ['y', 'd']) + z_cols = [f"Z{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((z, y, d)), columns=z_cols + ["y", "d"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', z_cols) + return DoubleMLData(data, "y", "d", z_cols) else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") else: u_t = np.random.uniform(low=0, high=1, size=n_obs) t = 1.0 * (u_t <= lambda_t) - y = t * y1 + (1-t)*y0 + y = t * y1 + (1 - t) * y0 if return_type in _array_alias: return z, y, d, t elif return_type in _data_frame_alias + _dml_data_alias: - z_cols = [f'Z{i + 1}' for i in np.arange(dim_x)] - data = pd.DataFrame(np.column_stack((z, y, d, t)), - columns=z_cols + ['y', 'd', 't']) + z_cols = [f"Z{i + 1}" for i in np.arange(dim_x)] + data = pd.DataFrame(np.column_stack((z, y, d, t)), columns=z_cols + ["y", "d", "t"]) if return_type in _data_frame_alias: return data else: - return DoubleMLData(data, 'y', 'd', z_cols, t_col='t') + return DoubleMLData(data, "y", "d", z_cols, t_col="t") else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") def make_confounded_irm_data(n_obs=500, theta=0.0, gamma_a=0.127, beta_a=0.58, linear=False, **kwargs): @@ -1000,26 +1152,33 @@ def make_confounded_irm_data(n_obs=500, theta=0.0, gamma_a=0.127, beta_a=0.58, l """ c = 0.0 # the confounding strength is only valid for c=0 xi = 0.75 - dim_x = kwargs.get('dim_x', 5) - trimming_threshold = kwargs.get('trimming_threshold', 0.01) - var_eps_y = kwargs.get('var_eps_y', 1.0) + dim_x = kwargs.get("dim_x", 5) + trimming_threshold = kwargs.get("trimming_threshold", 0.01) + var_eps_y = kwargs.get("var_eps_y", 1.0) # Specification of main regression function def f_reg(w): - res = 2.5 + 0.74*w[:, 0] + 0.25 * w[:, 1] + 0.137*(w[:, 2] + w[:, 3]) + res = 2.5 + 0.74 * w[:, 0] + 0.25 * w[:, 1] + 0.137 * (w[:, 2] + w[:, 3]) return res # Specification of prop score function def f_ps(w, xi): - res = xi*(-w[:, 0] + 0.1*w[:, 1] - 0.25*w[:, 2] - 0.1*w[:, 3]) + res = xi * (-w[:, 0] + 0.1 * w[:, 1] - 0.25 * w[:, 2] - 0.1 * w[:, 3]) return res + # observed covariates cov_mat = toeplitz([np.power(c, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) - z_tilde_1 = np.exp(0.5*x[:, 0]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) + z_tilde_1 = np.exp(0.5 * x[:, 0]) z_tilde_2 = 10 + x[:, 1] / (1 + np.exp(x[:, 0])) - z_tilde_3 = (0.6 + x[:, 0] * x[:, 2]/25)**3 - z_tilde_4 = (20 + x[:, 1] + x[:, 3])**2 + z_tilde_3 = (0.6 + x[:, 0] * x[:, 2] / 25) ** 3 + z_tilde_4 = (20 + x[:, 1] + x[:, 3]) ** 2 z_tilde_5 = x[:, 4] z_tilde = np.column_stack((z_tilde_1, z_tilde_2, z_tilde_3, z_tilde_4, z_tilde_5)) z = (z_tilde - np.mean(z_tilde, axis=0)) / np.std(z_tilde, axis=0) @@ -1040,36 +1199,38 @@ def f_ps(w, xi): p = np.exp(f_ps(features_ps, xi)) / (1 + np.exp(f_ps(features_ps, xi))) # compute short and long form of propensity score - m_long = p + gamma_a*a + m_long = p + gamma_a * a m_short = p # check propensity score bounds if np.any(m_long < trimming_threshold) or np.any(m_long > 1.0 - trimming_threshold): m_long = np.clip(m_long, trimming_threshold, 1.0 - trimming_threshold) m_short = np.clip(m_short, trimming_threshold, 1.0 - trimming_threshold) - warnings.warn(f'Propensity score is close to 0 or 1. ' - f'Trimming is at {trimming_threshold} and {1.0-trimming_threshold} is applied') + warnings.warn( + f"Propensity score is close to 0 or 1. " + f"Trimming is at {trimming_threshold} and {1.0-trimming_threshold} is applied" + ) # generate treatment based on long form u = np.random.uniform(low=0, high=1, size=n_obs) d = 1.0 * (m_long >= u) # add treatment heterogeneity d1x = z[:, 4] + 1 - var_dx = np.var(d*(d1x)) + var_dx = np.var(d * (d1x)) cov_adx = gamma_a * var_a # Outcome regression g_partial_reg = f_reg(features_reg) # short model g_short_d0 = g_partial_reg g_short_d1 = (theta + beta_a * cov_adx / var_dx) * d1x + g_partial_reg - g_short = d * g_short_d1 + (1.0-d) * g_short_d0 + g_short = d * g_short_d1 + (1.0 - d) * g_short_d0 # long model g_long_d0 = g_partial_reg + beta_a * a g_long_d1 = theta * d1x + g_partial_reg + beta_a * a - g_long = d * g_long_d1 + (1.0-d) * g_long_d0 + g_long = d * g_long_d1 + (1.0 - d) * g_long_d0 # Potential outcomes y_0 = g_long_d0 + eps_y y_1 = g_long_d1 + eps_y # Realized outcome - y = d * y_1 + (1.0-d) * y_0 + y = d * y_1 + (1.0 - d) * y_0 # In-sample values for confounding strength explained_residual_variance = np.square(g_long - g_short) residual_variance = np.square(y - g_short) @@ -1084,7 +1245,9 @@ def f_ps(w, xi): propensity_ratio_short = m_short / (1.0 - m_short) rr_short_ate = d / m_short - (1.0 - d) / (1.0 - m_short) rr_short_atte = treated_weight - np.multiply(untreated_weight, propensity_ratio_short) - cf_d_ate = (np.mean(1/(m_long * (1 - m_long))) - np.mean(1/(m_short * (1 - m_short)))) / np.mean(1/(m_long * (1 - m_long))) + cf_d_ate = (np.mean(1 / (m_long * (1 - m_long))) - np.mean(1 / (m_short * (1 - m_short)))) / np.mean( + 1 / (m_long * (1 - m_long)) + ) cf_d_atte = (np.mean(propensity_ratio_long) - np.mean(propensity_ratio_short)) / np.mean(propensity_ratio_long) if (beta_a == 0) | (gamma_a == 0): rho_ate = 0.0 @@ -1093,28 +1256,23 @@ def f_ps(w, xi): rho_ate = np.corrcoef((g_long - g_short), (rr_long_ate - rr_short_ate))[0, 1] rho_atte = np.corrcoef((g_long - g_short), (rr_long_atte - rr_short_atte))[0, 1] oracle_values = { - 'g_long': g_long, - 'g_short': g_short, - 'm_long': m_long, - 'm_short': m_short, - 'gamma_a': gamma_a, - 'beta_a': beta_a, - 'a': a, - 'y_0': y_0, - 'y_1': y_1, - 'z': z, - 'cf_y': cf_y, - 'cf_d_ate': cf_d_ate, - 'cf_d_atte': cf_d_atte, - 'rho_ate': rho_ate, - 'rho_atte': rho_atte, - } - res_dict = { - 'x': x, - 'y': y, - 'd': d, - 'oracle_values': oracle_values + "g_long": g_long, + "g_short": g_short, + "m_long": m_long, + "m_short": m_short, + "gamma_a": gamma_a, + "beta_a": beta_a, + "a": a, + "y_0": y_0, + "y_1": y_1, + "z": z, + "cf_y": cf_y, + "cf_d_ate": cf_d_ate, + "cf_d_atte": cf_d_atte, + "rho_ate": rho_ate, + "rho_atte": rho_atte, } + res_dict = {"x": x, "y": y, "d": d, "oracle_values": oracle_values} return res_dict @@ -1207,17 +1365,23 @@ def make_confounded_plr_data(n_obs=500, theta=5.0, cf_y=0.04, cf_d=0.04, **kwarg Doubly robust difference-in-differences estimators. Journal of Econometrics, 219(1), 101-122. doi:`10.1016/j.jeconom.2020.06.003 `_. """ - c = kwargs.get('c', 0.0) - dim_x = kwargs.get('dim_x', 4) + c = kwargs.get("c", 0.0) + dim_x = kwargs.get("dim_x", 4) # observed covariates cov_mat = toeplitz([np.power(c, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) - z_tilde_1 = np.exp(0.5*x[:, 0]) + z_tilde_1 = np.exp(0.5 * x[:, 0]) z_tilde_2 = 10 + x[:, 1] / (1 + np.exp(x[:, 0])) - z_tilde_3 = (0.6 + x[:, 0] * x[:, 2]/25)**3 - z_tilde_4 = (20 + x[:, 1] + x[:, 3])**2 + z_tilde_3 = (0.6 + x[:, 0] * x[:, 2] / 25) ** 3 + z_tilde_4 = (20 + x[:, 1] + x[:, 3]) ** 2 z_tilde = np.column_stack((z_tilde_1, z_tilde_2, z_tilde_3, z_tilde_4, x[:, 4:])) z = (z_tilde - np.mean(z_tilde, axis=0)) / np.std(z_tilde, axis=0) @@ -1234,7 +1398,7 @@ def make_confounded_plr_data(n_obs=500, theta=5.0, cf_y=0.04, cf_d=0.04, **kwarg var_a = np.square(a_bounds[1] - a_bounds[0]) / 12 # get the required impact of the confounder on the propensity score - m_short = -z[:, 0] + 0.5*z[:, 1] - 0.25*z[:, 2] - 0.1*z[:, 3] + m_short = -z[:, 0] + 0.5 * z[:, 1] - 0.25 * z[:, 2] - 0.1 * z[:, 3] def f_m(gamma_a): rr_long = eps_d / var_eps_d @@ -1243,40 +1407,39 @@ def f_m(gamma_a): return np.square(C2_D / (1 + C2_D) - cf_d) gamma_a = minimize_scalar(f_m).x - m_long = m_short + gamma_a*a + m_long = m_short + gamma_a * a d = m_long + eps_d # short and long version of g - g_partial_reg = 210 + 27.4*z[:, 0] + 13.7*(z[:, 1] + z[:, 2] + z[:, 3]) + g_partial_reg = 210 + 27.4 * z[:, 0] + 13.7 * (z[:, 1] + z[:, 2] + z[:, 3]) var_d = np.var(d) def f_g(beta_a): - g_diff = beta_a * (a - gamma_a * (var_a/var_d) * d) + g_diff = beta_a * (a - gamma_a * (var_a / var_d) * d) y_diff = eps_y + g_diff return np.square(np.mean(np.square(g_diff)) / np.mean(np.square(y_diff)) - cf_y) beta_a = minimize_scalar(f_g).x - g_long = theta*d + g_partial_reg + beta_a*a - g_short = (theta + gamma_a*beta_a * var_a / var_d)*d + g_partial_reg + g_long = theta * d + g_partial_reg + beta_a * a + g_short = (theta + gamma_a * beta_a * var_a / var_d) * d + g_partial_reg y = g_long + eps_y - oracle_values = {'g_long': g_long, - 'g_short': g_short, - 'm_long': m_long, - 'm_short': m_short, - 'theta': theta, - 'gamma_a': gamma_a, - 'beta_a': beta_a, - 'a': a, - 'z': z} - - res_dict = {'x': x, - 'y': y, - 'd': d, - 'oracle_values': oracle_values} + oracle_values = { + "g_long": g_long, + "g_short": g_short, + "m_long": m_long, + "m_short": m_short, + "theta": theta, + "gamma_a": gamma_a, + "beta_a": beta_a, + "a": a, + "z": z, + } + + res_dict = {"x": x, "y": y, "d": d, "oracle_values": oracle_values} return res_dict @@ -1345,14 +1508,16 @@ def make_heterogeneous_data(n_obs=200, p=30, support_size=5, n_x=1, binary_treat """ # simple input checks - assert n_x in [1, 2], 'n_x must be either 1 or 2.' - assert support_size <= p, 'support_size must be smaller than p.' - assert isinstance(binary_treatment, bool), 'binary_treatment must be a boolean.' + assert n_x in [1, 2], "n_x must be either 1 or 2." + assert support_size <= p, "support_size must be smaller than p." + assert isinstance(binary_treatment, bool), "binary_treatment must be a boolean." # define treatment effects if n_x == 1: + def treatment_effect(x): return np.exp(2 * x[:, 0]) + 3 * np.sin(4 * x[:, 0]) + else: assert n_x == 2 @@ -1382,23 +1547,16 @@ def treatment_effect(x): y = te * d + np.dot(x[:, support_y], coefs_y) + epsilon # Now we build the dataset - y_df = pd.DataFrame({'y': y}) - d_df = pd.DataFrame({'d': d}) - x_df = pd.DataFrame( - data=x, - index=np.arange(x.shape[0]), - columns=[f'X_{i}' for i in range(x.shape[1])] - ) + y_df = pd.DataFrame({"y": y}) + d_df = pd.DataFrame({"d": d}) + x_df = pd.DataFrame(data=x, index=np.arange(x.shape[0]), columns=[f"X_{i}" for i in range(x.shape[1])]) data = pd.concat([y_df, d_df, x_df], axis=1) - res_dict = { - 'data': data, - 'effects': te, - 'treatment_effect': treatment_effect} + res_dict = {"data": data, "effects": te, "treatment_effect": treatment_effect} return res_dict -def make_ssm_data(n_obs=8000, dim_x=100, theta=1, mar=True, return_type='DoubleMLData'): +def make_ssm_data(n_obs=8000, dim_x=100, theta=1, mar=True, return_type="DoubleMLData"): """ Generates data from a sample selection model (SSM). The data generating process is defined as @@ -1455,7 +1613,13 @@ def make_ssm_data(n_obs=8000, dim_x=100, theta=1, mar=True, return_type='DoubleM e = np.random.multivariate_normal(mean=[0, 0], cov=sigma, size=n_obs).T cov_mat = toeplitz([np.power(0.5, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) beta = [0.4 / (k**2) for k in range(1, dim_x + 1)] @@ -1469,21 +1633,19 @@ def make_ssm_data(n_obs=8000, dim_x=100, theta=1, mar=True, return_type='DoubleM if return_type in _array_alias: return x, y, d, z, s elif return_type in _data_frame_alias + _dml_data_alias: - x_cols = [f'X{i + 1}' for i in np.arange(dim_x)] + x_cols = [f"X{i + 1}" for i in np.arange(dim_x)] if mar: - data = pd.DataFrame(np.column_stack((x, y, d, s)), - columns=x_cols + ['y', 'd', 's']) + data = pd.DataFrame(np.column_stack((x, y, d, s)), columns=x_cols + ["y", "d", "s"]) else: - data = pd.DataFrame(np.column_stack((x, y, d, z, s)), - columns=x_cols + ['y', 'd', 'z', 's']) + data = pd.DataFrame(np.column_stack((x, y, d, z, s)), columns=x_cols + ["y", "d", "z", "s"]) if return_type in _data_frame_alias: return data else: if mar: - return DoubleMLData(data, 'y', 'd', x_cols, None, None, 's') - return DoubleMLData(data, 'y', 'd', x_cols, 'z', None, 's') + return DoubleMLData(data, "y", "d", x_cols, None, None, "s") + return DoubleMLData(data, "y", "d", x_cols, "z", None, "s") else: - raise ValueError('Invalid return_type.') + raise ValueError("Invalid return_type.") def make_irm_data_discrete_treatments(n_obs=200, n_levels=3, linear=False, random_state=None, **kwargs): @@ -1572,25 +1734,31 @@ def make_irm_data_discrete_treatments(n_obs=200, n_levels=3, linear=False, rando """ if random_state is not None: np.random.seed(random_state) - xi = kwargs.get('xi', 0.3) - c = kwargs.get('c', 0.0) - dim_x = kwargs.get('dim_x', 5) + xi = kwargs.get("xi", 0.3) + c = kwargs.get("c", 0.0) + dim_x = kwargs.get("dim_x", 5) if not isinstance(n_levels, int): - raise ValueError('n_levels must be an integer.') + raise ValueError("n_levels must be an integer.") if n_levels < 2: - raise ValueError('n_levels must be at least 2.') + raise ValueError("n_levels must be at least 2.") # observed covariates cov_mat = toeplitz([np.power(c, k) for k in range(dim_x)]) - x = np.random.multivariate_normal(np.zeros(dim_x), cov_mat, size=[n_obs, ]) + x = np.random.multivariate_normal( + np.zeros(dim_x), + cov_mat, + size=[ + n_obs, + ], + ) def f_reg(w): - res = 210 + 27.4*w[:, 0] + 13.7*(w[:, 1] + w[:, 2] + w[:, 3]) + res = 210 + 27.4 * w[:, 0] + 13.7 * (w[:, 1] + w[:, 2] + w[:, 3]) return res def f_treatment(w, xi): - res = xi * (-w[:, 0] + 0.5*w[:, 1] - 0.25*w[:, 2] - 0.1*w[:, 3]) + res = xi * (-w[:, 0] + 0.5 * w[:, 1] - 0.25 * w[:, 2] - 0.1 * w[:, 3]) return res def treatment_effect(d, scale=15): @@ -1598,8 +1766,8 @@ def treatment_effect(d, scale=15): z_tilde_1 = np.exp(0.5 * x[:, 0]) z_tilde_2 = 10 + x[:, 1] / (1 + np.exp(x[:, 0])) - z_tilde_3 = (0.6 + x[:, 0] * x[:, 2]/25)**3 - z_tilde_4 = (20 + x[:, 1] + x[:, 3])**2 + z_tilde_3 = (0.6 + x[:, 0] * x[:, 2] / 25) ** 3 + z_tilde_4 = (20 + x[:, 1] + x[:, 3]) ** 2 z_tilde = np.column_stack((z_tilde_1, z_tilde_2, z_tilde_3, z_tilde_4, x[:, 4:])) z = (z_tilde - np.mean(z_tilde, axis=0)) / np.std(z_tilde, axis=0) @@ -1622,7 +1790,7 @@ def treatment_effect(d, scale=15): level_bounds = np.quantile(cont_d, q=np.linspace(0, 1, n_levels + 1)) potential_level = sum([1.0 * (cont_d >= bound) for bound in level_bounds[1:-1]]) + 1 eta = np.random.uniform(0, 1, size=n_obs) - d = 1.0 * (eta >= 1/n_levels) * potential_level + d = 1.0 * (eta >= 1 / n_levels) * potential_level ite = treatment_effect(cont_d) y0 = g + eps_y @@ -1630,18 +1798,13 @@ def treatment_effect(d, scale=15): y = ite * (d > 0) + y0 oracle_values = { - 'cont_d': cont_d, - 'level_bounds': level_bounds, - 'potential_level': potential_level, - 'ite': ite, - 'y0': y0, + "cont_d": cont_d, + "level_bounds": level_bounds, + "potential_level": potential_level, + "ite": ite, + "y0": y0, } - resul_dict = { - 'x': x, - 'y': y, - 'd': d, - 'oracle_values': oracle_values - } + resul_dict = {"x": x, "y": y, "d": d, "oracle_values": oracle_values} return resul_dict diff --git a/doubleml/double_ml.py b/doubleml/double_ml.py index afab3f56c..59e496c10 100644 --- a/doubleml/double_ml.py +++ b/doubleml/double_ml.py @@ -14,27 +14,23 @@ from .utils.gain_statistics import gain_statistics from .utils.resampling import DoubleMLClusterResampling, DoubleMLResampling -_implemented_data_backends = ['DoubleMLData', 'DoubleMLClusterData'] +_implemented_data_backends = ["DoubleMLData", "DoubleMLClusterData"] class DoubleML(ABC): - """Double Machine Learning. - """ - - def __init__(self, - obj_dml_data, - n_folds, - n_rep, - score, - draw_sample_splitting): + """Double Machine Learning.""" + + def __init__(self, obj_dml_data, n_folds, n_rep, score, draw_sample_splitting): # check and pick up obj_dml_data if not isinstance(obj_dml_data, DoubleMLBaseData): - raise TypeError('The data must be of ' + ' or '.join(_implemented_data_backends) + ' type. ' - f'{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed.') + raise TypeError( + "The data must be of " + " or ".join(_implemented_data_backends) + " type. " + f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + ) self._is_cluster_data = False if isinstance(obj_dml_data, DoubleMLClusterData): if obj_dml_data.n_cluster_vars > 2: - raise NotImplementedError('Multi-way (n_ways > 2) clustering not yet implemented.') + raise NotImplementedError("Multi-way (n_ways > 2) clustering not yet implemented.") self._is_cluster_data = True self._dml_data = obj_dml_data @@ -64,27 +60,29 @@ def __init__(self, # check resampling specifications if not isinstance(n_folds, int): - raise TypeError('The number of folds must be of int type. ' - f'{str(n_folds)} of type {str(type(n_folds))} was passed.') + raise TypeError( + "The number of folds must be of int type. " f"{str(n_folds)} of type {str(type(n_folds))} was passed." + ) if n_folds < 1: - raise ValueError('The number of folds must be positive. ' - f'{str(n_folds)} was passed.') + raise ValueError("The number of folds must be positive. " f"{str(n_folds)} was passed.") if not isinstance(n_rep, int): - raise TypeError('The number of repetitions for the sample splitting must be of int type. ' - f'{str(n_rep)} of type {str(type(n_rep))} was passed.') + raise TypeError( + "The number of repetitions for the sample splitting must be of int type. " + f"{str(n_rep)} of type {str(type(n_rep))} was passed." + ) if n_rep < 1: - raise ValueError('The number of repetitions for the sample splitting must be positive. ' - f'{str(n_rep)} was passed.') + raise ValueError( + "The number of repetitions for the sample splitting must be positive. " f"{str(n_rep)} was passed." + ) if not isinstance(draw_sample_splitting, bool): - raise TypeError('draw_sample_splitting must be True or False. ' - f'Got {str(draw_sample_splitting)}.') + raise TypeError("draw_sample_splitting must be True or False. " f"Got {str(draw_sample_splitting)}.") # set resampling specifications if self._is_cluster_data: self._n_folds_per_cluster = n_folds - self._n_folds = n_folds ** self._dml_data.n_cluster_vars + self._n_folds = n_folds**self._dml_data.n_cluster_vars else: self._n_folds = n_folds self._n_rep = n_rep @@ -99,8 +97,16 @@ def __init__(self, self.draw_sample_splitting() # initialize arrays according to obj_dml_data and the resampling settings - self._psi, self._psi_deriv, self._psi_elements, self._var_scaling_factors, \ - self._coef, self._se, self._all_coef, self._all_se = self._initialize_arrays() + ( + self._psi, + self._psi_deriv, + self._psi_elements, + self._var_scaling_factors, + self._coef, + self._se, + self._all_coef, + self._all_se, + ) = self._initialize_arrays() # initialize instance attributes which are later used for iterating self._i_rep = None @@ -108,39 +114,47 @@ def __init__(self, def __str__(self): class_name = self.__class__.__name__ - header = f'================== {class_name} Object ==================\n' + header = f"================== {class_name} Object ==================\n" data_summary = self._dml_data._data_summary_str() - score_info = f'Score function: {str(self.score)}\n' - learner_info = '' + score_info = f"Score function: {str(self.score)}\n" + learner_info = "" for key, value in self.learner.items(): - learner_info += f'Learner {key}: {str(value)}\n' + learner_info += f"Learner {key}: {str(value)}\n" if self.nuisance_loss is not None: - learner_info += 'Out-of-sample Performance:\n' + learner_info += "Out-of-sample Performance:\n" is_classifier = [value for value in self._is_classifier.values()] is_regressor = [not value for value in is_classifier] if any(is_regressor): - learner_info += 'Regression:\n' + learner_info += "Regression:\n" for learner in [key for key, value in self._is_classifier.items() if value is False]: - learner_info += f'Learner {learner} RMSE: {self.nuisance_loss[learner]}\n' + learner_info += f"Learner {learner} RMSE: {self.nuisance_loss[learner]}\n" if any(is_classifier): - learner_info += 'Classification:\n' + learner_info += "Classification:\n" for learner in [key for key, value in self._is_classifier.items() if value is True]: - learner_info += f'Learner {learner} Log Loss: {self.nuisance_loss[learner]}\n' + learner_info += f"Learner {learner} Log Loss: {self.nuisance_loss[learner]}\n" if self._is_cluster_data: - resampling_info = f'No. folds per cluster: {self._n_folds_per_cluster}\n' \ - f'No. folds: {self.n_folds}\n' \ - f'No. repeated sample splits: {self.n_rep}\n' + resampling_info = ( + f"No. folds per cluster: {self._n_folds_per_cluster}\n" + f"No. folds: {self.n_folds}\n" + f"No. repeated sample splits: {self.n_rep}\n" + ) else: - resampling_info = f'No. folds: {self.n_folds}\n' \ - f'No. repeated sample splits: {self.n_rep}\n' + resampling_info = f"No. folds: {self.n_folds}\n" f"No. repeated sample splits: {self.n_rep}\n" fit_summary = str(self.summary) - res = header + \ - '\n------------------ Data summary ------------------\n' + data_summary + \ - '\n------------------ Score & algorithm ------------------\n' + score_info + \ - '\n------------------ Machine learner ------------------\n' + learner_info + \ - '\n------------------ Resampling ------------------\n' + resampling_info + \ - '\n------------------ Fit summary ------------------\n' + fit_summary + res = ( + header + + "\n------------------ Data summary ------------------\n" + + data_summary + + "\n------------------ Score & algorithm ------------------\n" + + score_info + + "\n------------------ Machine learner ------------------\n" + + learner_info + + "\n------------------ Resampling ------------------\n" + + resampling_info + + "\n------------------ Fit summary ------------------\n" + + fit_summary + ) return res @property @@ -266,8 +280,14 @@ def get_params(self, learner): """ valid_learner = self.params_names if (not isinstance(learner, str)) | (learner not in valid_learner): - raise ValueError('Invalid nuisance learner ' + str(learner) + '. ' + - 'Valid nuisance learner ' + ' or '.join(valid_learner) + '.') + raise ValueError( + "Invalid nuisance learner " + + str(learner) + + ". " + + "Valid nuisance learner " + + " or ".join(valid_learner) + + "." + ) return self._params[learner] # The private function _get_params delivers the single treatment, single (cross-fitting) sample subselection. @@ -283,8 +303,10 @@ def smpls(self): The partition used for cross-fitting. """ if self._smpls is None: - err_msg = ('Sample splitting not specified. Either draw samples via .draw_sample splitting() ' + - 'or set external samples via .set_sample_splitting().') + err_msg = ( + "Sample splitting not specified. Either draw samples via .draw_sample splitting() " + + "or set external samples via .set_sample_splitting()." + ) raise ValueError(err_msg) return self._smpls @@ -415,16 +437,12 @@ def summary(self): """ A summary for the estimated causal effect after calling :meth:`fit`. """ - col_names = ['coef', 'std err', 't', 'P>|t|'] + col_names = ["coef", "std err", "t", "P>|t|"] if np.isnan(self.coef).all(): df_summary = pd.DataFrame(columns=col_names) else: - summary_stats = np.transpose(np.vstack( - [self.coef, self.se, - self.t_stat, self.pval])) - df_summary = pd.DataFrame(summary_stats, - columns=col_names, - index=self._dml_data.d_cols) + summary_stats = np.transpose(np.vstack([self.coef, self.se, self.t_stat, self.pval])) + df_summary = pd.DataFrame(summary_stats, columns=col_names, index=self._dml_data.d_cols) ci = self.confint() df_summary = df_summary.join(ci) return df_summary @@ -498,10 +516,8 @@ def fit(self, n_jobs_cv=None, store_predictions=True, external_predictions=None, # predictions have to be stored in loop for sensitivity analysis nuisance_predictions = self._fit_nuisance_and_score_elements( - n_jobs_cv, - store_predictions, - external_predictions, - store_models) + n_jobs_cv, store_predictions, external_predictions, store_models + ) self._solve_score_and_estimate_se() @@ -532,42 +548,46 @@ def construct_framework(self): scaled_psi_reshape = np.transpose(scaled_psi, (0, 2, 1)) doubleml_dict = { - 'thetas': self.coef, - 'all_thetas': self.all_coef, - 'ses': self.se, - 'all_ses': self.all_se, - 'var_scaling_factors': self._var_scaling_factors, - 'scaled_psi': scaled_psi_reshape, - 'is_cluster_data': self._is_cluster_data + "thetas": self.coef, + "all_thetas": self.all_coef, + "ses": self.se, + "all_ses": self.all_se, + "var_scaling_factors": self._var_scaling_factors, + "scaled_psi": scaled_psi_reshape, + "is_cluster_data": self._is_cluster_data, } if self._sensitivity_implemented: # reshape sensitivity elements to (n_obs, n_coefs, n_rep) - doubleml_dict.update({ - 'sensitivity_elements': { - 'sigma2': np.transpose(self.sensitivity_elements['sigma2'], (0, 2, 1)), - 'nu2': np.transpose(self.sensitivity_elements['nu2'], (0, 2, 1)), - 'psi_sigma2': np.transpose(self.sensitivity_elements['psi_sigma2'], (0, 2, 1)), - 'psi_nu2': np.transpose(self.sensitivity_elements['psi_nu2'], (0, 2, 1)), - 'riesz_rep': np.transpose(self.sensitivity_elements['riesz_rep'], (0, 2, 1)) + doubleml_dict.update( + { + "sensitivity_elements": { + "sigma2": np.transpose(self.sensitivity_elements["sigma2"], (0, 2, 1)), + "nu2": np.transpose(self.sensitivity_elements["nu2"], (0, 2, 1)), + "psi_sigma2": np.transpose(self.sensitivity_elements["psi_sigma2"], (0, 2, 1)), + "psi_nu2": np.transpose(self.sensitivity_elements["psi_nu2"], (0, 2, 1)), + "riesz_rep": np.transpose(self.sensitivity_elements["riesz_rep"], (0, 2, 1)), + } } - }) + ) if self._is_cluster_data: - doubleml_dict.update({ - 'is_cluster_data': True, - 'cluster_dict': { - 'smpls': self._smpls, - 'smpls_cluster': self._smpls_cluster, - 'cluster_vars': self._dml_data.cluster_vars, - 'n_folds_per_cluster': self._n_folds_per_cluster, + doubleml_dict.update( + { + "is_cluster_data": True, + "cluster_dict": { + "smpls": self._smpls, + "smpls_cluster": self._smpls_cluster, + "cluster_vars": self._dml_data.cluster_vars, + "n_folds_per_cluster": self._n_folds_per_cluster, + }, } - }) + ) doubleml_framework = DoubleMLFramework(doubleml_dict) return doubleml_framework - def bootstrap(self, method='normal', n_rep_boot=500): + def bootstrap(self, method="normal", n_rep_boot=500): """ Multiplier bootstrap for DoubleML models. @@ -585,7 +605,7 @@ def bootstrap(self, method='normal', n_rep_boot=500): self : object """ if self._framework is None: - raise ValueError('Apply fit() before bootstrap().') + raise ValueError("Apply fit() before bootstrap().") self._framework.bootstrap(method=method, n_rep_boot=n_rep_boot) return self @@ -611,14 +631,14 @@ def confint(self, joint=False, level=0.95): """ if self.framework is None: - raise ValueError('Apply fit() before confint().') + raise ValueError("Apply fit() before confint().") df_ci = self.framework.confint(joint=joint, level=level) df_ci.set_index(pd.Index(self._dml_data.d_cols), inplace=True) return df_ci - def p_adjust(self, method='romano-wolf'): + def p_adjust(self, method="romano-wolf"): """ Multiple testing adjustment for DoubleML models. @@ -637,23 +657,25 @@ def p_adjust(self, method='romano-wolf'): """ if self.framework is None: - raise ValueError('Apply fit() before p_adjust().') + raise ValueError("Apply fit() before p_adjust().") p_val, _ = self.framework.p_adjust(method=method) p_val.set_index(pd.Index(self._dml_data.d_cols), inplace=True) return p_val - def tune(self, - param_grids, - tune_on_folds=False, - scoring_methods=None, # if None the estimator's score method is used - n_folds_tune=5, - search_mode='grid_search', - n_iter_randomized_search=100, - n_jobs_cv=None, - set_as_params=True, - return_tune_res=False): + def tune( + self, + param_grids, + tune_on_folds=False, + scoring_methods=None, # if None the estimator's score method is used + n_folds_tune=5, + search_mode="grid_search", + n_iter_randomized_search=100, + n_jobs_cv=None, + set_as_params=True, + return_tune_res=False, + ): """ Hyperparameter-tuning for DoubleML models. @@ -712,14 +734,22 @@ def tune(self, """ if (not isinstance(param_grids, dict)) | (not all(k in param_grids for k in self.learner_names)): - raise ValueError('Invalid param_grids ' + str(param_grids) + '. ' - 'param_grids must be a dictionary with keys ' + ' and '.join(self.learner_names) + '.') + raise ValueError( + "Invalid param_grids " + str(param_grids) + ". " + "param_grids must be a dictionary with keys " + " and ".join(self.learner_names) + "." + ) if scoring_methods is not None: if (not isinstance(scoring_methods, dict)) | (not all(k in self.learner_names for k in scoring_methods)): - raise ValueError('Invalid scoring_methods ' + str(scoring_methods) + '. ' + - 'scoring_methods must be a dictionary. ' + - 'Valid keys are ' + ' and '.join(self.learner_names) + '.') + raise ValueError( + "Invalid scoring_methods " + + str(scoring_methods) + + ". " + + "scoring_methods must be a dictionary. " + + "Valid keys are " + + " and ".join(self.learner_names) + + "." + ) if not all(k in scoring_methods for k in self.learner_names): # if there are learners for which no scoring_method was set, we fall back to None, i.e., default scoring for learner in self.learner_names: @@ -727,40 +757,43 @@ def tune(self, scoring_methods[learner] = None if not isinstance(tune_on_folds, bool): - raise TypeError('tune_on_folds must be True or False. ' - f'Got {str(tune_on_folds)}.') + raise TypeError("tune_on_folds must be True or False. " f"Got {str(tune_on_folds)}.") if not isinstance(n_folds_tune, int): - raise TypeError('The number of folds used for tuning must be of int type. ' - f'{str(n_folds_tune)} of type {str(type(n_folds_tune))} was passed.') + raise TypeError( + "The number of folds used for tuning must be of int type. " + f"{str(n_folds_tune)} of type {str(type(n_folds_tune))} was passed." + ) if n_folds_tune < 2: - raise ValueError('The number of folds used for tuning must be at least two. ' - f'{str(n_folds_tune)} was passed.') + raise ValueError("The number of folds used for tuning must be at least two. " f"{str(n_folds_tune)} was passed.") - if (not isinstance(search_mode, str)) | (search_mode not in ['grid_search', 'randomized_search']): - raise ValueError('search_mode must be "grid_search" or "randomized_search". ' - f'Got {str(search_mode)}.') + if (not isinstance(search_mode, str)) | (search_mode not in ["grid_search", "randomized_search"]): + raise ValueError('search_mode must be "grid_search" or "randomized_search". ' f"Got {str(search_mode)}.") if not isinstance(n_iter_randomized_search, int): - raise TypeError('The number of parameter settings sampled for the randomized search must be of int type. ' - f'{str(n_iter_randomized_search)} of type ' - f'{str(type(n_iter_randomized_search))} was passed.') + raise TypeError( + "The number of parameter settings sampled for the randomized search must be of int type. " + f"{str(n_iter_randomized_search)} of type " + f"{str(type(n_iter_randomized_search))} was passed." + ) if n_iter_randomized_search < 2: - raise ValueError('The number of parameter settings sampled for the randomized search must be at least two. ' - f'{str(n_iter_randomized_search)} was passed.') + raise ValueError( + "The number of parameter settings sampled for the randomized search must be at least two. " + f"{str(n_iter_randomized_search)} was passed." + ) if n_jobs_cv is not None: if not isinstance(n_jobs_cv, int): - raise TypeError('The number of CPUs used to fit the learners must be of int type. ' - f'{str(n_jobs_cv)} of type {str(type(n_jobs_cv))} was passed.') + raise TypeError( + "The number of CPUs used to fit the learners must be of int type. " + f"{str(n_jobs_cv)} of type {str(type(n_jobs_cv))} was passed." + ) if not isinstance(set_as_params, bool): - raise TypeError('set_as_params must be True or False. ' - f'Got {str(set_as_params)}.') + raise TypeError("set_as_params must be True or False. " f"Got {str(set_as_params)}.") if not isinstance(return_tune_res, bool): - raise TypeError('return_tune_res must be True or False. ' - f'Got {str(return_tune_res)}.') + raise TypeError("return_tune_res must be True or False. " f"Got {str(return_tune_res)}.") if tune_on_folds: tuning_res = [[None] * self.n_rep] * self._dml_data.n_treat @@ -779,14 +812,18 @@ def tune(self, self._i_rep = i_rep # tune hyperparameters - res = self._nuisance_tuning(self.__smpls, - param_grids, scoring_methods, - n_folds_tune, - n_jobs_cv, - search_mode, n_iter_randomized_search) + res = self._nuisance_tuning( + self.__smpls, + param_grids, + scoring_methods, + n_folds_tune, + n_jobs_cv, + search_mode, + n_iter_randomized_search, + ) tuning_res[i_rep][i_d] = res - nuisance_params.append(res['params']) + nuisance_params.append(res["params"]) if set_as_params: for nuisance_model in nuisance_params[0].keys(): @@ -796,16 +833,14 @@ def tune(self, else: smpls = [(np.arange(self._dml_data.n_obs), np.arange(self._dml_data.n_obs))] # tune hyperparameters - res = self._nuisance_tuning(smpls, - param_grids, scoring_methods, - n_folds_tune, - n_jobs_cv, - search_mode, n_iter_randomized_search) + res = self._nuisance_tuning( + smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ) tuning_res[i_d] = res if set_as_params: - for nuisance_model in res['params'].keys(): - params = res['params'][nuisance_model] + for nuisance_model in res["params"].keys(): + params = res["params"][nuisance_model] self.set_ml_nuisance_params(nuisance_model, self._dml_data.d_cols[i_d], params[0]) if return_tune_res: @@ -835,12 +870,19 @@ def set_ml_nuisance_params(self, learner, treat_var, params): """ valid_learner = self.params_names if learner not in valid_learner: - raise ValueError('Invalid nuisance learner ' + learner + '. ' + - 'Valid nuisance learner ' + ' or '.join(valid_learner) + '.') + raise ValueError( + "Invalid nuisance learner " + learner + ". " + "Valid nuisance learner " + " or ".join(valid_learner) + "." + ) if treat_var not in self._dml_data.d_cols: - raise ValueError('Invalid treatment variable ' + treat_var + '. ' + - 'Valid treatment variable ' + ' or '.join(self._dml_data.d_cols) + '.') + raise ValueError( + "Invalid treatment variable " + + treat_var + + ". " + + "Valid treatment variable " + + " or ".join(self._dml_data.d_cols) + + "." + ) if params is None: all_params = [None] * self.n_rep @@ -866,24 +908,25 @@ def _nuisance_est(self, smpls, n_jobs_cv, return_models, external_predictions): pass @abstractmethod - def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, - search_mode, n_iter_randomized_search): + def _nuisance_tuning( + self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search + ): pass @staticmethod def _check_learner(learner, learner_name, regressor, classifier): - err_msg_prefix = f'Invalid learner provided for {learner_name}: ' - warn_msg_prefix = f'Learner provided for {learner_name} is probably invalid: ' + err_msg_prefix = f"Invalid learner provided for {learner_name}: " + warn_msg_prefix = f"Learner provided for {learner_name} is probably invalid: " if isinstance(learner, type): - raise TypeError(err_msg_prefix + 'provide an instance of a learner instead of a class.') + raise TypeError(err_msg_prefix + "provide an instance of a learner instead of a class.") - if not hasattr(learner, 'fit'): - raise TypeError(err_msg_prefix + f'{str(learner)} has no method .fit().') - if not hasattr(learner, 'set_params'): - raise TypeError(err_msg_prefix + f'{str(learner)} has no method .set_params().') - if not hasattr(learner, 'get_params'): - raise TypeError(err_msg_prefix + f'{str(learner)} has no method .get_params().') + if not hasattr(learner, "fit"): + raise TypeError(err_msg_prefix + f"{str(learner)} has no method .fit().") + if not hasattr(learner, "set_params"): + raise TypeError(err_msg_prefix + f"{str(learner)} has no method .set_params().") + if not hasattr(learner, "get_params"): + raise TypeError(err_msg_prefix + f"{str(learner)} has no method .get_params().") if regressor & classifier: if is_classifier(learner): @@ -891,50 +934,55 @@ def _check_learner(learner, learner_name, regressor, classifier): elif is_regressor(learner): learner_is_classifier = False else: - warnings.warn(warn_msg_prefix + f'{str(learner)} is (probably) neither a regressor nor a classifier. ' + - 'Method predict is used for prediction.') + warnings.warn( + warn_msg_prefix + + f"{str(learner)} is (probably) neither a regressor nor a classifier. " + + "Method predict is used for prediction." + ) learner_is_classifier = False elif classifier: if not is_classifier(learner): - warnings.warn(warn_msg_prefix + f'{str(learner)} is (probably) no classifier.') + warnings.warn(warn_msg_prefix + f"{str(learner)} is (probably) no classifier.") learner_is_classifier = True else: assert regressor # classifier, regressor or both must be True if not is_regressor(learner): - warnings.warn(warn_msg_prefix + f'{str(learner)} is (probably) no regressor.') + warnings.warn(warn_msg_prefix + f"{str(learner)} is (probably) no regressor.") learner_is_classifier = False # check existence of the prediction method if learner_is_classifier: - if not hasattr(learner, 'predict_proba'): - raise TypeError(err_msg_prefix + f'{str(learner)} has no method .predict_proba().') + if not hasattr(learner, "predict_proba"): + raise TypeError(err_msg_prefix + f"{str(learner)} has no method .predict_proba().") else: - if not hasattr(learner, 'predict'): - raise TypeError(err_msg_prefix + f'{str(learner)} has no method .predict().') + if not hasattr(learner, "predict"): + raise TypeError(err_msg_prefix + f"{str(learner)} has no method .predict().") return learner_is_classifier def _check_fit(self, n_jobs_cv, store_predictions, external_predictions, store_models): if n_jobs_cv is not None: if not isinstance(n_jobs_cv, int): - raise TypeError('The number of CPUs used to fit the learners must be of int type. ' - f'{str(n_jobs_cv)} of type {str(type(n_jobs_cv))} was passed.') + raise TypeError( + "The number of CPUs used to fit the learners must be of int type. " + f"{str(n_jobs_cv)} of type {str(type(n_jobs_cv))} was passed." + ) if not isinstance(store_predictions, bool): - raise TypeError('store_predictions must be True or False. ' - f'Got {str(store_predictions)}.') + raise TypeError("store_predictions must be True or False. " f"Got {str(store_predictions)}.") if not isinstance(store_models, bool): - raise TypeError('store_models must be True or False. ' - f'Got {str(store_models)}.') + raise TypeError("store_models must be True or False. " f"Got {str(store_models)}.") # check if external predictions are implemented if self._external_predictions_implemented: - _check_external_predictions(external_predictions=external_predictions, - valid_treatments=self._dml_data.d_cols, - valid_learners=self.params_names, - n_obs=self._dml_data.n_obs, - n_rep=self.n_rep) + _check_external_predictions( + external_predictions=external_predictions, + valid_treatments=self._dml_data.d_cols, + valid_learners=self.params_names, + n_obs=self._dml_data.n_obs, + n_rep=self.n_rep, + ) elif not self._external_predictions_implemented and external_predictions is not None: raise NotImplementedError(f"External predictions not implemented for {self.__class__.__name__}.") @@ -949,46 +997,46 @@ def _initalize_fit(self, store_predictions, store_models): self._initialize_models() if self._sensitivity_implemented: - self._sensitivity_elements = self._initialize_sensitivity_elements((self._dml_data.n_obs, - self.n_rep, - self._dml_data.n_coefs)) + self._sensitivity_elements = self._initialize_sensitivity_elements( + (self._dml_data.n_obs, self.n_rep, self._dml_data.n_coefs) + ) def _fit_nuisance_and_score_elements(self, n_jobs_cv, store_predictions, external_predictions, store_models): - ext_prediction_dict = _set_external_predictions(external_predictions, - learners=self.params_names, - treatment=self._dml_data.d_cols[self._i_treat], - i_rep=self._i_rep) + ext_prediction_dict = _set_external_predictions( + external_predictions, learners=self.params_names, treatment=self._dml_data.d_cols[self._i_treat], i_rep=self._i_rep + ) # ml estimation of nuisance models and computation of score elements - score_elements, preds = self._nuisance_est(self.__smpls, n_jobs_cv, - external_predictions=ext_prediction_dict, - return_models=store_models) + score_elements, preds = self._nuisance_est( + self.__smpls, n_jobs_cv, external_predictions=ext_prediction_dict, return_models=store_models + ) self._set_score_elements(score_elements, self._i_rep, self._i_treat) # calculate nuisance losses and store predictions and targets of the nuisance models - self._calc_nuisance_loss(preds['predictions'], preds['targets']) + self._calc_nuisance_loss(preds["predictions"], preds["targets"]) if store_predictions: - self._store_predictions_and_targets(preds['predictions'], preds['targets']) + self._store_predictions_and_targets(preds["predictions"], preds["targets"]) if store_models: - self._store_models(preds['models']) + self._store_models(preds["models"]) return preds def _solve_score_and_estimate_se(self): # estimate the causal parameter - self._all_coef[self._i_treat, self._i_rep] = \ - self._est_causal_pars(self._get_score_elements(self._i_rep, self._i_treat)) + self._all_coef[self._i_treat, self._i_rep] = self._est_causal_pars( + self._get_score_elements(self._i_rep, self._i_treat) + ) # compute score (depends on the estimated causal parameter) self._psi[:, self._i_rep, self._i_treat] = self._compute_score( - self._get_score_elements(self._i_rep, self._i_treat), - self._all_coef[self._i_treat, self._i_rep]) + self._get_score_elements(self._i_rep, self._i_treat), self._all_coef[self._i_treat, self._i_rep] + ) # compute score derivative (can depend on the estimated causal parameter) self._psi_deriv[:, self._i_rep, self._i_treat] = self._compute_score_deriv( - self._get_score_elements(self._i_rep, self._i_treat), - self._all_coef[self._i_treat, self._i_rep]) + self._get_score_elements(self._i_rep, self._i_treat), self._all_coef[self._i_treat, self._i_rep] + ) # compute standard errors for causal parameter self._all_se[self._i_treat, self._i_rep], self._var_scaling_factors[self._i_treat] = self._se_causal_pars() @@ -996,7 +1044,7 @@ def _solve_score_and_estimate_se(self): def _fit_sensitivity_elements(self, nuisance_predictions): if self._sensitivity_implemented: if callable(self.score): - warnings.warn('Sensitivity analysis not implemented for callable scores.') + warnings.warn("Sensitivity analysis not implemented for callable scores.") else: # compute sensitivity analysis elements element_dict = self._sensitivity_element_est(nuisance_predictions) @@ -1020,20 +1068,22 @@ def _initialize_arrays(self): return psi, psi_deriv, psi_elements, var_scaling_factors, coef, se, all_coef, all_se def _initialize_predictions_and_targets(self): - self._predictions = {learner: np.full((self._dml_data.n_obs, self.n_rep, self._dml_data.n_coefs), np.nan) - for learner in self.params_names} - self._nuisance_targets = {learner: np.full((self._dml_data.n_obs, self.n_rep, self._dml_data.n_coefs), np.nan) - for learner in self.params_names} - - def _initialize_nuisance_loss(self): - self._nuisance_loss = { - learner: np.full((self.n_rep, self._dml_data.n_coefs), np.nan) + self._predictions = { + learner: np.full((self._dml_data.n_obs, self.n_rep, self._dml_data.n_coefs), np.nan) for learner in self.params_names } + self._nuisance_targets = { + learner: np.full((self._dml_data.n_obs, self.n_rep, self._dml_data.n_coefs), np.nan) + for learner in self.params_names + } + + def _initialize_nuisance_loss(self): + self._nuisance_loss = {learner: np.full((self.n_rep, self._dml_data.n_coefs), np.nan) for learner in self.params_names} def _initialize_models(self): - self._models = {learner: {treat_var: [None] * self.n_rep for treat_var in self._dml_data.d_cols} - for learner in self.params_names} + self._models = { + learner: {treat_var: [None] * self.n_rep for treat_var in self._dml_data.d_cols} for learner in self.params_names + } def _store_predictions_and_targets(self, preds, targets): for learner in self.params_names: @@ -1047,9 +1097,7 @@ def _calc_nuisance_loss(self, preds, targets): learner_keys = [key for key in self._learner.keys() if key in learner] assert len(learner_keys) == 1 self._is_classifier[learner] = self._check_learner( - self._learner[learner_keys[0]], - learner, - regressor=True, classifier=True + self._learner[learner_keys[0]], learner, regressor=True, classifier=True ) if targets[learner] is None: @@ -1121,27 +1169,28 @@ def evaluate_learners(self, learners=None, metric=_rmse): # check metric if not callable(metric): - raise TypeError('metric should be a callable. ' - '%r was passed.' % metric) + raise TypeError("metric should be a callable. " "%r was passed." % metric) if all(learner in self.params_names for learner in learners): if self.nuisance_targets is None: - raise ValueError('Apply fit() before evaluate_learners().') + raise ValueError("Apply fit() before evaluate_learners().") else: - dist = {learner: np.full((self.n_rep, self._dml_data.n_coefs), np.nan) - for learner in learners} + dist = {learner: np.full((self.n_rep, self._dml_data.n_coefs), np.nan) for learner in learners} for learner in learners: for rep in range(self.n_rep): for coef_idx in range(self._dml_data.n_coefs): - res = metric(y_pred=self.predictions[learner][:, rep, coef_idx].reshape(1, -1), - y_true=self.nuisance_targets[learner][:, rep, coef_idx].reshape(1, -1)) + res = metric( + y_pred=self.predictions[learner][:, rep, coef_idx].reshape(1, -1), + y_true=self.nuisance_targets[learner][:, rep, coef_idx].reshape(1, -1), + ) if not np.isfinite(res): - raise ValueError(f'Evaluation from learner {str(learner)} is not finite.') + raise ValueError(f"Evaluation from learner {str(learner)} is not finite.") dist[learner][rep, coef_idx] = res return dist else: - raise ValueError(f'The learners have to be a subset of {str(self.params_names)}. ' - f'Learners {str(learners)} provided.') + raise ValueError( + f"The learners have to be a subset of {str(self.params_names)}. " f"Learners {str(learners)} provided." + ) def draw_sample_splitting(self): """ @@ -1155,17 +1204,18 @@ def draw_sample_splitting(self): self : object """ if self._is_cluster_data: - obj_dml_resampling = DoubleMLClusterResampling(n_folds=self._n_folds_per_cluster, - n_rep=self.n_rep, - n_obs=self._dml_data.n_obs, - n_cluster_vars=self._dml_data.n_cluster_vars, - cluster_vars=self._dml_data.cluster_vars) + obj_dml_resampling = DoubleMLClusterResampling( + n_folds=self._n_folds_per_cluster, + n_rep=self.n_rep, + n_obs=self._dml_data.n_obs, + n_cluster_vars=self._dml_data.n_cluster_vars, + cluster_vars=self._dml_data.cluster_vars, + ) self._smpls, self._smpls_cluster = obj_dml_resampling.split_samples() else: - obj_dml_resampling = DoubleMLResampling(n_folds=self.n_folds, - n_rep=self.n_rep, - n_obs=self._dml_data.n_obs, - stratify=self._strata) + obj_dml_resampling = DoubleMLResampling( + n_folds=self.n_folds, n_rep=self.n_rep, n_obs=self._dml_data.n_obs, stratify=self._strata + ) self._smpls = obj_dml_resampling.split_samples() return self @@ -1231,10 +1281,19 @@ def set_sample_splitting(self, all_smpls, all_smpls_cluster=None): >>> dml_plr_obj.set_sample_splitting(smpls) """ self._smpls, self._smpls_cluster, self._n_rep, self._n_folds = _check_sample_splitting( - all_smpls, all_smpls_cluster, self._dml_data, self._is_cluster_data) + all_smpls, all_smpls_cluster, self._dml_data, self._is_cluster_data + ) - self._psi, self._psi_deriv, self._psi_elements, self._var_scaling_factors, \ - self._coef, self._se, self._all_coef, self._all_se = self._initialize_arrays() + ( + self._psi, + self._psi_deriv, + self._psi_elements, + self._var_scaling_factors, + self._coef, + self._se, + self._all_coef, + self._all_se, + ) = self._initialize_arrays() self._initialize_ml_nuisance_params() return self @@ -1245,10 +1304,10 @@ def _est_causal_pars(self, psi_elements): if not self._is_cluster_data: coef = self._est_coef(psi_elements) else: - scaling_factor = [1.] * len(smpls) + scaling_factor = [1.0] * len(smpls) for i_fold, (_, _) in enumerate(smpls): test_cluster_inds = self.__smpls_cluster[i_fold][1] - scaling_factor[i_fold] = 1./np.prod(np.array([len(inds) for inds in test_cluster_inds])) + scaling_factor[i_fold] = 1.0 / np.prod(np.array([len(inds) for inds in test_cluster_inds])) coef = self._est_coef(psi_elements, smpls=smpls, scaling_factor=scaling_factor) return coef @@ -1264,13 +1323,15 @@ def _se_causal_pars(self): smpls_cluster = self.__smpls_cluster n_folds_per_cluster = self._n_folds_per_cluster - sigma2_hat, var_scaling_factor = _var_est(psi=self.__psi, - psi_deriv=self.__psi_deriv, - smpls=self.__smpls, - is_cluster_data=self._is_cluster_data, - cluster_vars=cluster_vars, - smpls_cluster=smpls_cluster, - n_folds_per_cluster=n_folds_per_cluster) + sigma2_hat, var_scaling_factor = _var_est( + psi=self.__psi, + psi_deriv=self.__psi_deriv, + smpls=self.__smpls, + is_cluster_data=self._is_cluster_data, + cluster_vars=cluster_vars, + smpls_cluster=smpls_cluster, + n_folds_per_cluster=n_folds_per_cluster, + ) se = np.sqrt(sigma2_hat) return se, var_scaling_factor @@ -1283,18 +1344,19 @@ def _est_causal_pars_and_se(self): self._i_treat = i_d # estimate the causal parameter - self._all_coef[self._i_treat, self._i_rep] = \ - self._est_causal_pars(self._get_score_elements(self._i_rep, self._i_treat)) + self._all_coef[self._i_treat, self._i_rep] = self._est_causal_pars( + self._get_score_elements(self._i_rep, self._i_treat) + ) # compute score (depends on the estimated causal parameter) self._psi[:, self._i_rep, self._i_treat] = self._compute_score( - self._get_score_elements(self._i_rep, self._i_treat), - self._all_coef[self._i_treat, self._i_rep]) + self._get_score_elements(self._i_rep, self._i_treat), self._all_coef[self._i_treat, self._i_rep] + ) # compute score (can depend on the estimated causal parameter) self._psi_deriv[:, self._i_rep, self._i_treat] = self._compute_score_deriv( - self._get_score_elements(self._i_rep, self._i_treat), - self._all_coef[self._i_treat, self._i_rep]) + self._get_score_elements(self._i_rep, self._i_treat), self._all_coef[self._i_treat, self._i_rep] + ) # compute standard errors for causal parameter self._all_se[self._i_treat, self._i_rep], self._var_scaling_factors[self._i_treat] = self._se_causal_pars() @@ -1326,12 +1388,15 @@ def _get_score_elements(self, i_rep, i_treat): def _set_score_elements(self, psi_elements, i_rep, i_treat): if not isinstance(psi_elements, dict): - raise TypeError('_ml_nuisance_and_score_elements must return score elements in a dict. ' - f'Got type {str(type(psi_elements))}.') + raise TypeError( + "_ml_nuisance_and_score_elements must return score elements in a dict. " f"Got type {str(type(psi_elements))}." + ) if not (set(self._score_element_names) == set(psi_elements.keys())): - raise ValueError('_ml_nuisance_and_score_elements returned incomplete score elements. ' - 'Expected dict with keys: ' + ' and '.join(set(self._score_element_names)) + '.' - 'Got dict with keys: ' + ' and '.join(set(psi_elements.keys())) + '.') + raise ValueError( + "_ml_nuisance_and_score_elements returned incomplete score elements. " + "Expected dict with keys: " + " and ".join(set(self._score_element_names)) + "." + "Got dict with keys: " + " and ".join(set(psi_elements.keys())) + "." + ) for key in self._score_element_names: self.psi_elements[key][:, i_rep, i_treat] = psi_elements[key] return @@ -1347,15 +1412,17 @@ def _sensitivity_element_est(self, preds): @property def _sensitivity_element_names(self): - return ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2', 'riesz_rep'] + return ["sigma2", "nu2", "psi_sigma2", "psi_nu2", "riesz_rep"] # the dimensions will usually be (n_obs, n_rep, n_coefs) to be equal to the score dimensions psi def _initialize_sensitivity_elements(self, score_dim): - sensitivity_elements = {'sigma2': np.full((1, score_dim[1], score_dim[2]), np.nan), - 'nu2': np.full((1, score_dim[1], score_dim[2]), np.nan), - 'psi_sigma2': np.full(score_dim, np.nan), - 'psi_nu2': np.full(score_dim, np.nan), - 'riesz_rep': np.full(score_dim, np.nan)} + sensitivity_elements = { + "sigma2": np.full((1, score_dim[1], score_dim[2]), np.nan), + "nu2": np.full((1, score_dim[1], score_dim[2]), np.nan), + "psi_sigma2": np.full(score_dim, np.nan), + "psi_nu2": np.full(score_dim, np.nan), + "riesz_rep": np.full(score_dim, np.nan), + } return sensitivity_elements def _get_sensitivity_elements(self, i_rep, i_treat): @@ -1364,12 +1431,16 @@ def _get_sensitivity_elements(self, i_rep, i_treat): def _set_sensitivity_elements(self, sensitivity_elements, i_rep, i_treat): if not isinstance(sensitivity_elements, dict): - raise TypeError('_sensitivity_element_est must return sensitivity elements in a dict. ' - f'Got type {str(type(sensitivity_elements))}.') + raise TypeError( + "_sensitivity_element_est must return sensitivity elements in a dict. " + f"Got type {str(type(sensitivity_elements))}." + ) if not (set(self._sensitivity_element_names) == set(sensitivity_elements.keys())): - raise ValueError('_sensitivity_element_est returned incomplete sensitivity elements. ' - 'Expected dict with keys: ' + ' and '.join(set(self._sensitivity_element_names)) + '. ' - 'Got dict with keys: ' + ' and '.join(set(sensitivity_elements.keys())) + '.') + raise ValueError( + "_sensitivity_element_est returned incomplete sensitivity elements. " + "Expected dict with keys: " + " and ".join(set(self._sensitivity_element_names)) + ". " + "Got dict with keys: " + " and ".join(set(sensitivity_elements.keys())) + "." + ) for key in self._sensitivity_element_names: self.sensitivity_elements[key][:, i_rep, i_treat] = sensitivity_elements[key] return @@ -1412,14 +1483,8 @@ def sensitivity_analysis(self, cf_y=0.03, cf_d=0.03, rho=1.0, level=0.95, null_h """ if self._framework is None: - raise ValueError('Apply fit() before sensitivity_analysis().') - self._framework.sensitivity_analysis( - cf_y=cf_y, - cf_d=cf_d, - rho=rho, - level=level, - null_hypothesis=null_hypothesis - ) + raise ValueError("Apply fit() before sensitivity_analysis().") + self._framework.sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, rho=rho, level=level, null_hypothesis=null_hypothesis) return self @@ -1434,13 +1499,24 @@ def sensitivity_summary(self): Summary for the sensitivity analysis. """ if self._framework is None: - raise ValueError('Apply sensitivity_analysis() before sensitivity_summary.') + raise ValueError("Apply sensitivity_analysis() before sensitivity_summary.") else: sensitivity_summary = self._framework.sensitivity_summary return sensitivity_summary - def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, null_hypothesis=0.0, - include_scenario=True, benchmarks=None, fill=True, grid_bounds=(0.15, 0.15), grid_size=100): + def sensitivity_plot( + self, + idx_treatment=0, + value="theta", + rho=1.0, + level=0.95, + null_hypothesis=0.0, + include_scenario=True, + benchmarks=None, + fill=True, + grid_bounds=(0.15, 0.15), + grid_size=100, + ): """ Contour plot of the sensivity with respect to latent/confounding variables. @@ -1494,7 +1570,7 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, Plotly figure of the sensitivity contours. """ if self._framework is None: - raise ValueError('Apply fit() before sensitivity_plot().') + raise ValueError("Apply fit() before sensitivity_plot().") fig = self._framework.sensitivity_plot( idx_treatment=idx_treatment, value=value, @@ -1505,7 +1581,7 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, benchmarks=benchmarks, fill=fill, grid_bounds=grid_bounds, - grid_size=grid_size + grid_size=grid_size, ) return fig @@ -1523,18 +1599,20 @@ def sensitivity_benchmark(self, benchmarking_set, fit_args=None): # input checks if self._sensitivity_elements is None: - raise NotImplementedError(f'Sensitivity analysis not yet implemented for {self.__class__.__name__}.') + raise NotImplementedError(f"Sensitivity analysis not yet implemented for {self.__class__.__name__}.") if not isinstance(benchmarking_set, list): - raise TypeError('benchmarking_set must be a list. ' - f'{str(benchmarking_set)} of type {type(benchmarking_set)} was passed.') + raise TypeError( + "benchmarking_set must be a list. " f"{str(benchmarking_set)} of type {type(benchmarking_set)} was passed." + ) if len(benchmarking_set) == 0: - raise ValueError('benchmarking_set must not be empty.') + raise ValueError("benchmarking_set must not be empty.") if not set(benchmarking_set) <= set(x_list_long): - raise ValueError(f"benchmarking_set must be a subset of features {str(self._dml_data.x_cols)}. " - f'{str(benchmarking_set)} was passed.') + raise ValueError( + f"benchmarking_set must be a subset of features {str(self._dml_data.x_cols)}. " + f"{str(benchmarking_set)} was passed." + ) if fit_args is not None and not isinstance(fit_args, dict): - raise TypeError('fit_args must be a dict. ' - f'{str(fit_args)} of type {type(fit_args)} was passed.') + raise TypeError("fit_args must be a dict. " f"{str(fit_args)} of type {type(fit_args)} was passed.") # refit short form of the model x_list_short = [x for x in x_list_long if x not in benchmarking_set] diff --git a/doubleml/double_ml_data.py b/doubleml/double_ml_data.py index 95e067fcb..552e7e795 100644 --- a/doubleml/double_ml_data.py +++ b/doubleml/double_ml_data.py @@ -12,16 +12,13 @@ class DoubleMLBaseData(ABC): - """Base Class Double machine learning data-backends - """ - def __init__(self, - data): + """Base Class Double machine learning data-backends""" + + def __init__(self, data): if not isinstance(data, pd.DataFrame): - raise TypeError('data must be of pd.DataFrame type. ' - f'{str(data)} of type {str(type(data))} was passed.') + raise TypeError("data must be of pd.DataFrame type. " f"{str(data)} of type {str(type(data))} was passed.") if not data.columns.is_unique: - raise ValueError('Invalid pd.DataFrame: ' - 'Contains duplicate column names.') + raise ValueError("Invalid pd.DataFrame: " "Contains duplicate column names.") self._data = data def __str__(self): @@ -29,13 +26,17 @@ def __str__(self): buf = io.StringIO() self.data.info(verbose=False, buf=buf) df_info = buf.getvalue() - res = '================== DoubleMLBaseData Object ==================\n' + \ - '\n------------------ Data summary ------------------\n' + data_summary + \ - '\n------------------ DataFrame info ------------------\n' + df_info + res = ( + "================== DoubleMLBaseData Object ==================\n" + + "\n------------------ Data summary ------------------\n" + + data_summary + + "\n------------------ DataFrame info ------------------\n" + + df_info + ) return res def _data_summary_str(self): - data_summary = f'No. Observations: {self.n_obs}\n' + data_summary = f"No. Observations: {self.n_obs}\n" return data_summary @property @@ -63,7 +64,7 @@ def n_obs(self): # multiple treatment variables case) and other things are also build around it, see for example DoubleML._params @property def d_cols(self): - return ['theta'] + return ["theta"] @property def n_treat(self): @@ -137,16 +138,19 @@ class DoubleMLData(DoubleMLBaseData): >>> (x, y, d) = make_plr_CCDDHNR2018(return_type='array') >>> obj_dml_data_from_array = DoubleMLData.from_arrays(x, y, d) """ - def __init__(self, - data, - y_col, - d_cols, - x_cols=None, - z_cols=None, - t_col=None, - s_col=None, - use_other_treat_as_covariate=True, - force_all_x_finite=True): + + def __init__( + self, + data, + y_col, + d_cols, + x_cols=None, + z_cols=None, + t_col=None, + s_col=None, + use_other_treat_as_covariate=True, + force_all_x_finite=True, + ): DoubleMLBaseData.__init__(self, data) self.y_col = y_col @@ -169,26 +173,31 @@ def __str__(self): buf = io.StringIO() self.data.info(verbose=False, buf=buf) df_info = buf.getvalue() - res = '================== DoubleMLData Object ==================\n' + \ - '\n------------------ Data summary ------------------\n' + data_summary + \ - '\n------------------ DataFrame info ------------------\n' + df_info + res = ( + "================== DoubleMLData Object ==================\n" + + "\n------------------ Data summary ------------------\n" + + data_summary + + "\n------------------ DataFrame info ------------------\n" + + df_info + ) return res def _data_summary_str(self): - data_summary = f'Outcome variable: {self.y_col}\n' \ - f'Treatment variable(s): {self.d_cols}\n' \ - f'Covariates: {self.x_cols}\n' \ - f'Instrument variable(s): {self.z_cols}\n' + data_summary = ( + f"Outcome variable: {self.y_col}\n" + f"Treatment variable(s): {self.d_cols}\n" + f"Covariates: {self.x_cols}\n" + f"Instrument variable(s): {self.z_cols}\n" + ) if self.t_col is not None: - data_summary += f'Time variable: {self.t_col}\n' + data_summary += f"Time variable: {self.t_col}\n" if self.s_col is not None: - data_summary += f'Score/Selection variable: {self.s_col}\n' - data_summary += f'No. Observations: {self.n_obs}\n' + data_summary += f"Score/Selection variable: {self.s_col}\n" + data_summary += f"No. Observations: {self.n_obs}\n" return data_summary @classmethod - def from_arrays(cls, x, y, d, z=None, t=None, s=None, use_other_treat_as_covariate=True, - force_all_x_finite=True): + def from_arrays(cls, x, y, d, z=None, t=None, s=None, use_other_treat_as_covariate=True, force_all_x_finite=True): """ Initialize :class:`DoubleMLData` from :class:`numpy.ndarray`'s. @@ -236,22 +245,24 @@ def from_arrays(cls, x, y, d, z=None, t=None, s=None, use_other_treat_as_covaria >>> obj_dml_data_from_array = DoubleMLData.from_arrays(x, y, d) """ if isinstance(force_all_x_finite, str): - if force_all_x_finite != 'allow-nan': - raise ValueError("Invalid force_all_x_finite " + force_all_x_finite + ". " + - "force_all_x_finite must be True, False or 'allow-nan'.") + if force_all_x_finite != "allow-nan": + raise ValueError( + "Invalid force_all_x_finite " + + force_all_x_finite + + ". " + + "force_all_x_finite must be True, False or 'allow-nan'." + ) elif not isinstance(force_all_x_finite, bool): - raise TypeError("Invalid force_all_x_finite. " + - "force_all_x_finite must be True, False or 'allow-nan'.") + raise TypeError("Invalid force_all_x_finite. " + "force_all_x_finite must be True, False or 'allow-nan'.") - x = check_array(x, ensure_2d=False, allow_nd=False, - force_all_finite=force_all_x_finite) + x = check_array(x, ensure_2d=False, allow_nd=False, force_all_finite=force_all_x_finite) d = check_array(d, ensure_2d=False, allow_nd=False) y = column_or_1d(y, warn=True) x = _assure_2d_array(x) d = _assure_2d_array(d) - y_col = 'y' + y_col = "y" if z is None: check_consistent_length(x, y, d) z_cols = None @@ -260,34 +271,33 @@ def from_arrays(cls, x, y, d, z=None, t=None, s=None, use_other_treat_as_covaria z = _assure_2d_array(z) check_consistent_length(x, y, d, z) if z.shape[1] == 1: - z_cols = ['z'] + z_cols = ["z"] else: - z_cols = [f'z{i + 1}' for i in np.arange(z.shape[1])] + z_cols = [f"z{i + 1}" for i in np.arange(z.shape[1])] if t is None: t_col = None else: t = column_or_1d(t, warn=True) check_consistent_length(x, y, d, t) - t_col = 't' + t_col = "t" if s is None: s_col = None else: s = column_or_1d(s, warn=True) check_consistent_length(x, y, d, s) - s_col = 's' + s_col = "s" if d.shape[1] == 1: - d_cols = ['d'] + d_cols = ["d"] else: - d_cols = [f'd{i+1}' for i in np.arange(d.shape[1])] + d_cols = [f"d{i+1}" for i in np.arange(d.shape[1])] - x_cols = [f'X{i+1}' for i in np.arange(x.shape[1])] + x_cols = [f"X{i+1}" for i in np.arange(x.shape[1])] # basline version with features, outcome and treatments - data = pd.DataFrame(np.column_stack((x, y, d)), - columns=x_cols + [y_col] + d_cols) + data = pd.DataFrame(np.column_stack((x, y, d)), columns=x_cols + [y_col] + d_cols) if z is not None: df_z = pd.DataFrame(z, columns=z_cols) @@ -406,24 +416,24 @@ def x_cols(self): @x_cols.setter def x_cols(self, value): - reset_value = hasattr(self, '_x_cols') + reset_value = hasattr(self, "_x_cols") if value is not None: if isinstance(value, str): value = [value] if not isinstance(value, list): - raise TypeError('The covariates x_cols must be of str or list type (or None). ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The covariates x_cols must be of str or list type (or None). " + f"{str(value)} of type {str(type(value))} was passed." + ) if not len(set(value)) == len(value): - raise ValueError('Invalid covariates x_cols: ' - 'Contains duplicate values.') + raise ValueError("Invalid covariates x_cols: " "Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError('Invalid covariates x_cols. ' - 'At least one covariate is no data column.') + raise ValueError("Invalid covariates x_cols. " "At least one covariate is no data column.") assert set(value).issubset(set(self.all_variables)) self._x_cols = value else: excluded_cols = set.union({self.y_col}, set(self.d_cols)) - if (self.z_cols is not None): + if self.z_cols is not None: excluded_cols = set.union(excluded_cols, set(self.z_cols)) for col in [self.t_col, self.s_col]: col = _check_set(col) @@ -443,18 +453,18 @@ def d_cols(self): @d_cols.setter def d_cols(self, value): - reset_value = hasattr(self, '_d_cols') + reset_value = hasattr(self, "_d_cols") if isinstance(value, str): value = [value] if not isinstance(value, list): - raise TypeError('The treatment variable(s) d_cols must be of str or list type. ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The treatment variable(s) d_cols must be of str or list type. " + f"{str(value)} of type {str(type(value))} was passed." + ) if not len(set(value)) == len(value): - raise ValueError('Invalid treatment variable(s) d_cols: ' - 'Contains duplicate values.') + raise ValueError("Invalid treatment variable(s) d_cols: " "Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError('Invalid treatment variable(s) d_cols. ' - 'At least one treatment variable is no data column.') + raise ValueError("Invalid treatment variable(s) d_cols. " "At least one treatment variable is no data column.") self._d_cols = value if reset_value: self._check_disjoint_sets() @@ -470,13 +480,13 @@ def y_col(self): @y_col.setter def y_col(self, value): - reset_value = hasattr(self, '_y_col') + reset_value = hasattr(self, "_y_col") if not isinstance(value, str): - raise TypeError('The outcome variable y_col must be of str type. ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The outcome variable y_col must be of str type. " f"{str(value)} of type {str(type(value))} was passed." + ) if value not in self.all_variables: - raise ValueError('Invalid outcome variable y_col. ' - f'{value} is no data column.') + raise ValueError("Invalid outcome variable y_col. " f"{value} is no data column.") self._y_col = value if reset_value: self._check_disjoint_sets() @@ -491,19 +501,21 @@ def z_cols(self): @z_cols.setter def z_cols(self, value): - reset_value = hasattr(self, '_z_cols') + reset_value = hasattr(self, "_z_cols") if value is not None: if isinstance(value, str): value = [value] if not isinstance(value, list): - raise TypeError('The instrumental variable(s) z_cols must be of str or list type (or None). ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The instrumental variable(s) z_cols must be of str or list type (or None). " + f"{str(value)} of type {str(type(value))} was passed." + ) if not len(set(value)) == len(value): - raise ValueError('Invalid instrumental variable(s) z_cols: ' - 'Contains duplicate values.') + raise ValueError("Invalid instrumental variable(s) z_cols: " "Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError('Invalid instrumental variable(s) z_cols. ' - 'At least one instrumental variable is no data column.') + raise ValueError( + "Invalid instrumental variable(s) z_cols. " "At least one instrumental variable is no data column." + ) self._z_cols = value else: self._z_cols = None @@ -520,14 +532,15 @@ def t_col(self): @t_col.setter def t_col(self, value): - reset_value = hasattr(self, '_t_col') + reset_value = hasattr(self, "_t_col") if value is not None: if not isinstance(value, str): - raise TypeError('The time variable t_col must be of str type (or None). ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The time variable t_col must be of str type (or None). " + f"{str(value)} of type {str(type(value))} was passed." + ) if value not in self.all_variables: - raise ValueError('Invalid time variable t_col. ' - f'{value} is no data column.') + raise ValueError("Invalid time variable t_col. " f"{value} is no data column.") self._t_col = value else: self._t_col = None @@ -544,14 +557,15 @@ def s_col(self): @s_col.setter def s_col(self, value): - reset_value = hasattr(self, '_s_col') + reset_value = hasattr(self, "_s_col") if value is not None: if not isinstance(value, str): - raise TypeError('The score or selection variable s_col must be of str type (or None). ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The score or selection variable s_col must be of str type (or None). " + f"{str(value)} of type {str(type(value))} was passed." + ) if value not in self.all_variables: - raise ValueError('Invalid score or selection variable s_col. ' - f'{value} is no data column.') + raise ValueError("Invalid score or selection variable s_col. " f"{value} is no data column.") self._s_col = value else: self._s_col = None @@ -568,10 +582,9 @@ def use_other_treat_as_covariate(self): @use_other_treat_as_covariate.setter def use_other_treat_as_covariate(self, value): - reset_value = hasattr(self, '_use_other_treat_as_covariate') + reset_value = hasattr(self, "_use_other_treat_as_covariate") if not isinstance(value, bool): - raise TypeError('use_other_treat_as_covariate must be True or False. ' - f'Got {str(value)}.') + raise TypeError("use_other_treat_as_covariate must be True or False. " f"Got {str(value)}.") self._use_other_treat_as_covariate = value if reset_value: # by default, we initialize to the first treatment variable @@ -586,14 +599,14 @@ def force_all_x_finite(self): @force_all_x_finite.setter def force_all_x_finite(self, value): - reset_value = hasattr(self, '_force_all_x_finite') + reset_value = hasattr(self, "_force_all_x_finite") if isinstance(value, str): - if value != 'allow-nan': - raise ValueError("Invalid force_all_x_finite " + value + ". " + - "force_all_x_finite must be True, False or 'allow-nan'.") + if value != "allow-nan": + raise ValueError( + "Invalid force_all_x_finite " + value + ". " + "force_all_x_finite must be True, False or 'allow-nan'." + ) elif not isinstance(value, bool): - raise TypeError("Invalid force_all_x_finite. " + - "force_all_x_finite must be True, False or 'allow-nan'.") + raise TypeError("Invalid force_all_x_finite. " + "force_all_x_finite must be True, False or 'allow-nan'.") self._force_all_x_finite = value if reset_value: # by default, we initialize to the first treatment variable @@ -630,11 +643,11 @@ def set_x_d(self, treatment_var): Active treatment variable that will be set to d. """ if not isinstance(treatment_var, str): - raise TypeError('treatment_var must be of str type. ' - f'{str(treatment_var)} of type {str(type(treatment_var))} was passed.') + raise TypeError( + "treatment_var must be of str type. " f"{str(treatment_var)} of type {str(type(treatment_var))} was passed." + ) if treatment_var not in self.d_cols: - raise ValueError('Invalid treatment_var. ' - f'{treatment_var} is not in d_cols.') + raise ValueError("Invalid treatment_var. " f"{treatment_var} is not in d_cols.") if self.use_other_treat_as_covariate: # note that the following line needs to be adapted in case an intersection of x_cols and d_cols as allowed # (see https://github.com/DoubleML/doubleml-for-py/issues/83) @@ -644,8 +657,7 @@ def set_x_d(self, treatment_var): xd_list = self.x_cols assert_all_finite(self.data.loc[:, treatment_var]) if self.force_all_x_finite: - assert_all_finite(self.data.loc[:, xd_list], - allow_nan=self.force_all_x_finite == 'allow-nan') + assert_all_finite(self.data.loc[:, xd_list], allow_nan=self.force_all_x_finite == "allow-nan") self._d = self.data.loc[:, treatment_var] self._X = self.data.loc[:, xd_list] @@ -653,16 +665,16 @@ def _check_binary_treats(self): is_binary = pd.Series(dtype=bool, index=self.d_cols) for treatment_var in self.d_cols: this_d = self.data.loc[:, treatment_var] - binary_treat = (type_of_target(this_d) == 'binary') + binary_treat = type_of_target(this_d) == "binary" zero_one_treat = np.all((np.power(this_d, 2) - this_d) == 0) - is_binary[treatment_var] = (binary_treat & zero_one_treat) + is_binary[treatment_var] = binary_treat & zero_one_treat return is_binary def _check_binary_outcome(self): y = self.data.loc[:, self.y_col] - binary_outcome = (type_of_target(y) == 'binary') + binary_outcome = type_of_target(y) == "binary" zero_one_outcome = np.all((np.power(y, 2) - y) == 0) - is_binary = (binary_outcome & zero_one_outcome) + is_binary = binary_outcome & zero_one_outcome return is_binary def _check_disjoint_sets(self): @@ -675,28 +687,35 @@ def _check_disjoint_sets_y_d_x_z_t_s(self): d_cols_set = set(self.d_cols) if not y_col_set.isdisjoint(x_cols_set): - raise ValueError(f'{str(self.y_col)} cannot be set as outcome variable ``y_col`` and covariate in ' - '``x_cols``.') + raise ValueError(f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and covariate in " "``x_cols``.") if not y_col_set.isdisjoint(d_cols_set): - raise ValueError(f'{str(self.y_col)} cannot be set as outcome variable ``y_col`` and treatment variable in ' - '``d_cols``.') + raise ValueError( + f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and treatment variable in " "``d_cols``." + ) # note that the line xd_list = self.x_cols + self.d_cols in method set_x_d needs adaption if an intersection of # x_cols and d_cols as allowed (see https://github.com/DoubleML/doubleml-for-py/issues/83) if not d_cols_set.isdisjoint(x_cols_set): - raise ValueError('At least one variable/column is set as treatment variable (``d_cols``) and as covariate' - '(``x_cols``). Consider using parameter ``use_other_treat_as_covariate``.') + raise ValueError( + "At least one variable/column is set as treatment variable (``d_cols``) and as covariate" + "(``x_cols``). Consider using parameter ``use_other_treat_as_covariate``." + ) if self.z_cols is not None: z_cols_set = set(self.z_cols) if not y_col_set.isdisjoint(z_cols_set): - raise ValueError(f'{str(self.y_col)} cannot be set as outcome variable ``y_col`` and instrumental ' - 'variable in ``z_cols``.') + raise ValueError( + f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and instrumental " + "variable in ``z_cols``." + ) if not d_cols_set.isdisjoint(z_cols_set): - raise ValueError('At least one variable/column is set as treatment variable (``d_cols``) and ' - 'instrumental variable in ``z_cols``.') + raise ValueError( + "At least one variable/column is set as treatment variable (``d_cols``) and " + "instrumental variable in ``z_cols``." + ) if not x_cols_set.isdisjoint(z_cols_set): - raise ValueError('At least one variable/column is set as covariate (``x_cols``) and instrumental ' - 'variable in ``z_cols``.') + raise ValueError( + "At least one variable/column is set as covariate (``x_cols``) and instrumental " "variable in ``z_cols``." + ) self._check_disjoint_sets_t_s() @@ -708,41 +727,53 @@ def _check_disjoint_sets_t_s(self): if self.t_col is not None: t_col_set = {self.t_col} if not t_col_set.isdisjoint(x_cols_set): - raise ValueError(f'{str(self.t_col)} cannot be set as time variable ``t_col`` and covariate in ' - '``x_cols``.') + raise ValueError(f"{str(self.t_col)} cannot be set as time variable ``t_col`` and covariate in " "``x_cols``.") if not t_col_set.isdisjoint(d_cols_set): - raise ValueError(f'{str(self.t_col)} cannot be set as time variable ``t_col`` and treatment variable in ' - '``d_cols``.') + raise ValueError( + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and treatment variable in " "``d_cols``." + ) if not t_col_set.isdisjoint(y_col_set): - raise ValueError(f'{str(self.t_col)} cannot be set as time variable ``t_col`` and outcome variable ' - '``y_col``.') + raise ValueError( + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and outcome variable " "``y_col``." + ) if self.z_cols is not None: z_cols_set = set(self.z_cols) if not t_col_set.isdisjoint(z_cols_set): - raise ValueError(f'{str(self.t_col)} cannot be set as time variable ``t_col`` and instrumental ' - 'variable in ``z_cols``.') + raise ValueError( + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and instrumental " + "variable in ``z_cols``." + ) if self.s_col is not None: s_col_set = {self.s_col} if not s_col_set.isdisjoint(x_cols_set): - raise ValueError(f'{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and covariate in ' - '``x_cols``.') + raise ValueError( + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and covariate in " "``x_cols``." + ) if not s_col_set.isdisjoint(d_cols_set): - raise ValueError(f'{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and treatment ' - 'variable in ``d_cols``.') + raise ValueError( + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and treatment " + "variable in ``d_cols``." + ) if not s_col_set.isdisjoint(y_col_set): - raise ValueError(f'{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and outcome ' - 'variable ``y_col``.') + raise ValueError( + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and outcome " + "variable ``y_col``." + ) if self.z_cols is not None: z_cols_set = set(self.z_cols) if not s_col_set.isdisjoint(z_cols_set): - raise ValueError(f'{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and ' - 'instrumental variable in ``z_cols``.') + raise ValueError( + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and " + "instrumental variable in ``z_cols``." + ) if self.t_col is not None: t_col_set = {self.t_col} if not s_col_set.isdisjoint(t_col_set): - raise ValueError(f'{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and time ' - 'variable ``t_col``.') + raise ValueError( + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and time " + "variable ``t_col``." + ) class DoubleMLClusterData(DoubleMLData): @@ -807,32 +838,28 @@ class DoubleMLClusterData(DoubleMLData): >>> (x, y, d, cluster_vars, z) = make_pliv_multiway_cluster_CKMS2021(return_type='array') >>> obj_dml_data_from_array = DoubleMLClusterData.from_arrays(x, y, d, cluster_vars, z) """ - def __init__(self, - data, - y_col, - d_cols, - cluster_cols, - x_cols=None, - z_cols=None, - t_col=None, - s_col=None, - use_other_treat_as_covariate=True, - force_all_x_finite=True): + + def __init__( + self, + data, + y_col, + d_cols, + cluster_cols, + x_cols=None, + z_cols=None, + t_col=None, + s_col=None, + use_other_treat_as_covariate=True, + force_all_x_finite=True, + ): DoubleMLBaseData.__init__(self, data) # we need to set cluster_cols (needs _data) before call to the super __init__ because of the x_cols setter self.cluster_cols = cluster_cols self._set_cluster_vars() - DoubleMLData.__init__(self, - data, - y_col, - d_cols, - x_cols, - z_cols, - t_col, - s_col, - use_other_treat_as_covariate, - force_all_x_finite) + DoubleMLData.__init__( + self, data, y_col, d_cols, x_cols, z_cols, t_col, s_col, use_other_treat_as_covariate, force_all_x_finite + ) self._check_disjoint_sets_cluster_cols() def __str__(self): @@ -840,28 +867,35 @@ def __str__(self): buf = io.StringIO() self.data.info(verbose=False, buf=buf) df_info = buf.getvalue() - res = '================== DoubleMLClusterData Object ==================\n' + \ - '\n------------------ Data summary ------------------\n' + data_summary + \ - '\n------------------ DataFrame info ------------------\n' + df_info + res = ( + "================== DoubleMLClusterData Object ==================\n" + + "\n------------------ Data summary ------------------\n" + + data_summary + + "\n------------------ DataFrame info ------------------\n" + + df_info + ) return res def _data_summary_str(self): - data_summary = f'Outcome variable: {self.y_col}\n' \ - f'Treatment variable(s): {self.d_cols}\n' \ - f'Cluster variable(s): {self.cluster_cols}\n' \ - f'Covariates: {self.x_cols}\n' \ - f'Instrument variable(s): {self.z_cols}\n' + data_summary = ( + f"Outcome variable: {self.y_col}\n" + f"Treatment variable(s): {self.d_cols}\n" + f"Cluster variable(s): {self.cluster_cols}\n" + f"Covariates: {self.x_cols}\n" + f"Instrument variable(s): {self.z_cols}\n" + ) if self.t_col is not None: - data_summary += f'Time variable: {self.t_col}\n' + data_summary += f"Time variable: {self.t_col}\n" if self.s_col is not None: - data_summary += f'Score/Selection variable: {self.s_col}\n' + data_summary += f"Score/Selection variable: {self.s_col}\n" - data_summary += f'No. Observations: {self.n_obs}\n' + data_summary += f"No. Observations: {self.n_obs}\n" return data_summary @classmethod - def from_arrays(cls, x, y, d, cluster_vars, z=None, t=None, s=None, use_other_treat_as_covariate=True, - force_all_x_finite=True): + def from_arrays( + cls, x, y, d, cluster_vars, z=None, t=None, s=None, use_other_treat_as_covariate=True, force_all_x_finite=True + ): """ Initialize :class:`DoubleMLClusterData` from :class:`numpy.ndarray`'s. @@ -915,15 +949,24 @@ def from_arrays(cls, x, y, d, cluster_vars, z=None, t=None, s=None, use_other_tr cluster_vars = check_array(cluster_vars, ensure_2d=False, allow_nd=False) cluster_vars = _assure_2d_array(cluster_vars) if cluster_vars.shape[1] == 1: - cluster_cols = ['cluster_var'] + cluster_cols = ["cluster_var"] else: - cluster_cols = [f'cluster_var{i + 1}' for i in np.arange(cluster_vars.shape[1])] + cluster_cols = [f"cluster_var{i + 1}" for i in np.arange(cluster_vars.shape[1])] data = pd.concat((pd.DataFrame(cluster_vars, columns=cluster_cols), dml_data.data), axis=1) - return (cls(data, dml_data.y_col, dml_data.d_cols, cluster_cols, - dml_data.x_cols, dml_data.z_cols, dml_data.t_col, dml_data.s_col, - dml_data.use_other_treat_as_covariate, dml_data.force_all_x_finite)) + return cls( + data, + dml_data.y_col, + dml_data.d_cols, + cluster_cols, + dml_data.x_cols, + dml_data.z_cols, + dml_data.t_col, + dml_data.s_col, + dml_data.use_other_treat_as_covariate, + dml_data.force_all_x_finite, + ) @property def cluster_cols(self): @@ -934,18 +977,18 @@ def cluster_cols(self): @cluster_cols.setter def cluster_cols(self, value): - reset_value = hasattr(self, '_cluster_cols') + reset_value = hasattr(self, "_cluster_cols") if isinstance(value, str): value = [value] if not isinstance(value, list): - raise TypeError('The cluster variable(s) cluster_cols must be of str or list type. ' - f'{str(value)} of type {str(type(value))} was passed.') + raise TypeError( + "The cluster variable(s) cluster_cols must be of str or list type. " + f"{str(value)} of type {str(type(value))} was passed." + ) if not len(set(value)) == len(value): - raise ValueError('Invalid cluster variable(s) cluster_cols: ' - 'Contains duplicate values.') + raise ValueError("Invalid cluster variable(s) cluster_cols: " "Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError('Invalid cluster variable(s) cluster_cols. ' - 'At least one cluster variable is no data column.') + raise ValueError("Invalid cluster variable(s) cluster_cols. " "At least one cluster variable is no data column.") self._cluster_cols = value if reset_value: self._check_disjoint_sets() @@ -986,8 +1029,9 @@ def x_cols(self, value): x_cols = [col for col in self.data.columns if col not in y_d] else: if (self.z_cols is not None) & (self.t_col is not None): - y_d_z_t_s = set.union({self.y_col}, set(self.d_cols), set(self.z_cols), {self.t_col}, {self.s_col}, - set(self.cluster_cols)) + y_d_z_t_s = set.union( + {self.y_col}, set(self.d_cols), set(self.z_cols), {self.t_col}, {self.s_col}, set(self.cluster_cols) + ) x_cols = [col for col in self.data.columns if col not in y_d_z_t_s] elif self.z_cols is not None: y_d_z_s = set.union({self.y_col}, set(self.d_cols), set(self.z_cols), {self.s_col}, set(self.cluster_cols)) @@ -1019,28 +1063,37 @@ def _check_disjoint_sets_cluster_cols(self): s_col_set = {self.s_col} if not y_col_set.isdisjoint(cluster_cols_set): - raise ValueError(f'{str(self.y_col)} cannot be set as outcome variable ``y_col`` and cluster ' - 'variable in ``cluster_cols``.') + raise ValueError( + f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and cluster " "variable in ``cluster_cols``." + ) if not d_cols_set.isdisjoint(cluster_cols_set): - raise ValueError('At least one variable/column is set as treatment variable (``d_cols``) and ' - 'cluster variable in ``cluster_cols``.') + raise ValueError( + "At least one variable/column is set as treatment variable (``d_cols``) and " + "cluster variable in ``cluster_cols``." + ) # TODO: Is the following combination allowed, or not? if not x_cols_set.isdisjoint(cluster_cols_set): - raise ValueError('At least one variable/column is set as covariate (``x_cols``) and cluster ' - 'variable in ``cluster_cols``.') + raise ValueError( + "At least one variable/column is set as covariate (``x_cols``) and cluster " "variable in ``cluster_cols``." + ) if self.z_cols is not None: z_cols_set = set(self.z_cols) if not z_cols_set.isdisjoint(cluster_cols_set): - raise ValueError('At least one variable/column is set as instrumental variable (``z_cols``) and ' - 'cluster variable in ``cluster_cols``.') + raise ValueError( + "At least one variable/column is set as instrumental variable (``z_cols``) and " + "cluster variable in ``cluster_cols``." + ) if self.t_col is not None: if not t_col_set.isdisjoint(cluster_cols_set): - raise ValueError(f'{str(self.t_col)} cannot be set as time variable ``t_col`` and ' - 'cluster variable in ``cluster_cols``.') + raise ValueError( + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and " "cluster variable in ``cluster_cols``." + ) if self.s_col is not None: if not s_col_set.isdisjoint(cluster_cols_set): - raise ValueError(f'{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and ' - 'cluster variable in ``cluster_cols``.') + raise ValueError( + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and " + "cluster variable in ``cluster_cols``." + ) def _set_cluster_vars(self): assert_all_finite(self.data.loc[:, self.cluster_cols]) diff --git a/doubleml/double_ml_framework.py b/doubleml/double_ml_framework.py index d4af13c60..ba350a287 100644 --- a/doubleml/double_ml_framework.py +++ b/doubleml/double_ml_framework.py @@ -20,7 +20,7 @@ from .utils._plots import _sensitivity_contour_plot -class DoubleMLFramework(): +class DoubleMLFramework: """Double Machine Learning Framework to combine DoubleML classes and compute confidendence intervals. Parameters @@ -33,29 +33,29 @@ class DoubleMLFramework(): """ def __init__( - self, - doubleml_dict=None, + self, + doubleml_dict=None, ): self._is_cluster_data = False # check input if not isinstance(doubleml_dict, dict): - raise TypeError('doubleml_dict must be a dictionary.') - expected_keys = ['thetas', 'ses', 'all_thetas', 'all_ses', 'var_scaling_factors', 'scaled_psi'] + raise TypeError("doubleml_dict must be a dictionary.") + expected_keys = ["thetas", "ses", "all_thetas", "all_ses", "var_scaling_factors", "scaled_psi"] if not all(key in doubleml_dict.keys() for key in expected_keys): - raise ValueError('The dict must contain the following keys: ' + ', '.join(expected_keys)) + raise ValueError("The dict must contain the following keys: " + ", ".join(expected_keys)) # set scores and parameters - self._n_thetas = doubleml_dict['scaled_psi'].shape[1] - self._n_rep = doubleml_dict['scaled_psi'].shape[2] - self._n_obs = doubleml_dict['scaled_psi'].shape[0] + self._n_thetas = doubleml_dict["scaled_psi"].shape[1] + self._n_rep = doubleml_dict["scaled_psi"].shape[2] + self._n_obs = doubleml_dict["scaled_psi"].shape[0] - self._thetas = doubleml_dict['thetas'] - self._ses = doubleml_dict['ses'] - self._all_thetas = doubleml_dict['all_thetas'] - self._all_ses = doubleml_dict['all_ses'] - self._var_scaling_factors = doubleml_dict['var_scaling_factors'] - self._scaled_psi = doubleml_dict['scaled_psi'] + self._thetas = doubleml_dict["thetas"] + self._ses = doubleml_dict["ses"] + self._all_thetas = doubleml_dict["all_thetas"] + self._all_ses = doubleml_dict["all_ses"] + self._var_scaling_factors = doubleml_dict["var_scaling_factors"] + self._scaled_psi = doubleml_dict["scaled_psi"] # initialize cluster data self._check_and_set_cluster_data(doubleml_dict) @@ -67,9 +67,9 @@ def __init__( self._check_framework_shapes() self._treatment_names = None - if 'treatment_names' in doubleml_dict.keys(): - self._check_treatment_names(doubleml_dict['treatment_names']) - self._treatment_names = doubleml_dict['treatment_names'] + if "treatment_names" in doubleml_dict.keys(): + self._check_treatment_names(doubleml_dict["treatment_names"]) + self._treatment_names = doubleml_dict["treatment_names"] # initialize bootstrap distribution self._boot_t_stat = None @@ -228,8 +228,7 @@ def summary(self): A summary for the estimated causal parameters ``thetas``. """ ci = self.confint() - df_summary = generate_summary(self.thetas, self.ses, self.t_stats, - self.pvals, ci, self._treatment_names) + df_summary = generate_summary(self.thetas, self.ses, self.t_stats, self.pvals, ci, self._treatment_names) return df_summary @property @@ -242,42 +241,54 @@ def sensitivity_summary(self): res : str Summary for the sensitivity analysis. """ - header = '================== Sensitivity Analysis ==================\n' + header = "================== Sensitivity Analysis ==================\n" if self.sensitivity_params is None: - res = header + 'Apply sensitivity_analysis() to generate sensitivity_summary.' + res = header + "Apply sensitivity_analysis() to generate sensitivity_summary." else: sig_level = f'Significance Level: level={self.sensitivity_params["input"]["level"]}\n' - scenario_params = f'Sensitivity parameters: cf_y={self.sensitivity_params["input"]["cf_y"]}; ' \ - f'cf_d={self.sensitivity_params["input"]["cf_d"]}, ' \ - f'rho={self.sensitivity_params["input"]["rho"]}' - - theta_and_ci_col_names = ['CI lower', 'theta lower', ' theta', 'theta upper', 'CI upper'] - theta_and_ci = np.transpose(np.vstack((self.sensitivity_params['ci']['lower'], - self.sensitivity_params['theta']['lower'], - self.thetas, - self.sensitivity_params['theta']['upper'], - self.sensitivity_params['ci']['upper']))) - df_theta_and_ci = pd.DataFrame(theta_and_ci, - columns=theta_and_ci_col_names, - index=self.treatment_names) + scenario_params = ( + f'Sensitivity parameters: cf_y={self.sensitivity_params["input"]["cf_y"]}; ' + f'cf_d={self.sensitivity_params["input"]["cf_d"]}, ' + f'rho={self.sensitivity_params["input"]["rho"]}' + ) + + theta_and_ci_col_names = ["CI lower", "theta lower", " theta", "theta upper", "CI upper"] + theta_and_ci = np.transpose( + np.vstack( + ( + self.sensitivity_params["ci"]["lower"], + self.sensitivity_params["theta"]["lower"], + self.thetas, + self.sensitivity_params["theta"]["upper"], + self.sensitivity_params["ci"]["upper"], + ) + ) + ) + df_theta_and_ci = pd.DataFrame(theta_and_ci, columns=theta_and_ci_col_names, index=self.treatment_names) theta_and_ci_summary = str(df_theta_and_ci) - rvs_col_names = ['H_0', 'RV (%)', 'RVa (%)'] - rvs = np.transpose(np.vstack((self.sensitivity_params['rv'], - self.sensitivity_params['rva']))) * 100 + rvs_col_names = ["H_0", "RV (%)", "RVa (%)"] + rvs = np.transpose(np.vstack((self.sensitivity_params["rv"], self.sensitivity_params["rva"]))) * 100 - df_rvs = pd.DataFrame(np.column_stack((self.sensitivity_params["input"]["null_hypothesis"], rvs)), - columns=rvs_col_names, - index=self.treatment_names) + df_rvs = pd.DataFrame( + np.column_stack((self.sensitivity_params["input"]["null_hypothesis"], rvs)), + columns=rvs_col_names, + index=self.treatment_names, + ) rvs_summary = str(df_rvs) - res = header + \ - '\n------------------ Scenario ------------------\n' + \ - sig_level + scenario_params + '\n' + \ - '\n------------------ Bounds with CI ------------------\n' + \ - theta_and_ci_summary + '\n' + \ - '\n------------------ Robustness Values ------------------\n' + \ - rvs_summary + res = ( + header + + "\n------------------ Scenario ------------------\n" + + sig_level + + scenario_params + + "\n" + + "\n------------------ Bounds with CI ------------------\n" + + theta_and_ci_summary + + "\n" + + "\n------------------ Robustness Values ------------------\n" + + rvs_summary + ) return res @@ -297,39 +308,41 @@ def __add__(self, other): var_scaling_factors = self._var_scaling_factors # compute standard errors - sigma2_hat = np.divide( - np.mean(np.square(scaled_psi), axis=0), - var_scaling_factors.reshape(-1, 1)) + sigma2_hat = np.divide(np.mean(np.square(scaled_psi), axis=0), var_scaling_factors.reshape(-1, 1)) all_ses = np.sqrt(sigma2_hat) thetas, ses = _aggregate_coefs_and_ses(all_thetas, all_ses, var_scaling_factors) doubleml_dict = { - 'thetas': thetas, - 'ses': ses, - 'all_thetas': all_thetas, - 'all_ses': all_ses, - 'var_scaling_factors': var_scaling_factors, - 'scaled_psi': scaled_psi, - 'is_cluster_data': self._is_cluster_data, - 'cluster_dict': self._cluster_dict, + "thetas": thetas, + "ses": ses, + "all_thetas": all_thetas, + "all_ses": all_ses, + "var_scaling_factors": var_scaling_factors, + "scaled_psi": scaled_psi, + "is_cluster_data": self._is_cluster_data, + "cluster_dict": self._cluster_dict, } # sensitivity combination only available for same outcome and cond. expectation (e.g. IRM) if self._sensitivity_implemented and other._sensitivity_implemented: - nu2_score_element = self._sensitivity_elements['psi_nu2'] + other._sensitivity_elements['psi_nu2'] - \ - np.multiply(2.0, np.multiply(self._sensitivity_elements['riesz_rep'], - self._sensitivity_elements['riesz_rep'])) + nu2_score_element = ( + self._sensitivity_elements["psi_nu2"] + + other._sensitivity_elements["psi_nu2"] + - np.multiply( + 2.0, np.multiply(self._sensitivity_elements["riesz_rep"], self._sensitivity_elements["riesz_rep"]) + ) + ) nu2 = np.mean(nu2_score_element, axis=0, keepdims=True) psi_nu2 = nu2_score_element - nu2 sensitivity_elements = { - 'sigma2': self._sensitivity_elements['sigma2'], - 'nu2': nu2, - 'psi_sigma2': self._sensitivity_elements['psi_sigma2'], - 'psi_nu2': psi_nu2, - 'riesz_rep': self._sensitivity_elements['riesz_rep'] + other._sensitivity_elements['riesz_rep'], + "sigma2": self._sensitivity_elements["sigma2"], + "nu2": nu2, + "psi_sigma2": self._sensitivity_elements["psi_sigma2"], + "psi_nu2": psi_nu2, + "riesz_rep": self._sensitivity_elements["riesz_rep"] + other._sensitivity_elements["riesz_rep"], } - doubleml_dict['sensitivity_elements'] = sensitivity_elements + doubleml_dict["sensitivity_elements"] = sensitivity_elements new_obj = DoubleMLFramework(doubleml_dict) else: @@ -356,39 +369,41 @@ def __sub__(self, other): var_scaling_factors = self._var_scaling_factors # compute standard errors - sigma2_hat = np.divide( - np.mean(np.square(scaled_psi), axis=0), - var_scaling_factors.reshape(-1, 1)) + sigma2_hat = np.divide(np.mean(np.square(scaled_psi), axis=0), var_scaling_factors.reshape(-1, 1)) all_ses = np.sqrt(sigma2_hat) thetas, ses = _aggregate_coefs_and_ses(all_thetas, all_ses, var_scaling_factors) doubleml_dict = { - 'thetas': thetas, - 'ses': ses, - 'all_thetas': all_thetas, - 'all_ses': all_ses, - 'var_scaling_factors': var_scaling_factors, - 'scaled_psi': scaled_psi, - 'is_cluster_data': self._is_cluster_data, - 'cluster_dict': self._cluster_dict, + "thetas": thetas, + "ses": ses, + "all_thetas": all_thetas, + "all_ses": all_ses, + "var_scaling_factors": var_scaling_factors, + "scaled_psi": scaled_psi, + "is_cluster_data": self._is_cluster_data, + "cluster_dict": self._cluster_dict, } # sensitivity combination only available for same outcome and cond. expectation (e.g. IRM) if self._sensitivity_implemented and other._sensitivity_implemented: - nu2_score_element = self._sensitivity_elements['psi_nu2'] - other._sensitivity_elements['psi_nu2'] + \ - np.multiply(2.0, np.multiply(self._sensitivity_elements['riesz_rep'], - self._sensitivity_elements['riesz_rep'])) + nu2_score_element = ( + self._sensitivity_elements["psi_nu2"] + - other._sensitivity_elements["psi_nu2"] + + np.multiply( + 2.0, np.multiply(self._sensitivity_elements["riesz_rep"], self._sensitivity_elements["riesz_rep"]) + ) + ) nu2 = np.mean(nu2_score_element, axis=0, keepdims=True) psi_nu2 = nu2_score_element - nu2 sensitivity_elements = { - 'sigma2': self._sensitivity_elements['sigma2'], - 'nu2': nu2, - 'psi_sigma2': self._sensitivity_elements['psi_sigma2'], - 'psi_nu2': psi_nu2, - 'riesz_rep': self._sensitivity_elements['riesz_rep'] - other._sensitivity_elements['riesz_rep'], + "sigma2": self._sensitivity_elements["sigma2"], + "nu2": nu2, + "psi_sigma2": self._sensitivity_elements["psi_sigma2"], + "psi_nu2": psi_nu2, + "riesz_rep": self._sensitivity_elements["riesz_rep"] - other._sensitivity_elements["riesz_rep"], } - doubleml_dict['sensitivity_elements'] = sensitivity_elements + doubleml_dict["sensitivity_elements"] = sensitivity_elements new_obj = DoubleMLFramework(doubleml_dict) else: @@ -411,30 +426,30 @@ def __mul__(self, other): scaled_psi = np.multiply(other, self._scaled_psi) doubleml_dict = { - 'thetas': thetas, - 'ses': ses, - 'all_thetas': all_thetas, - 'all_ses': all_ses, - 'var_scaling_factors': var_scaling_factors, - 'scaled_psi': scaled_psi, - 'is_cluster_data': self._is_cluster_data, - 'cluster_dict': self._cluster_dict, + "thetas": thetas, + "ses": ses, + "all_thetas": all_thetas, + "all_ses": all_ses, + "var_scaling_factors": var_scaling_factors, + "scaled_psi": scaled_psi, + "is_cluster_data": self._is_cluster_data, + "cluster_dict": self._cluster_dict, } # sensitivity combination only available for linear models if self._sensitivity_implemented: - nu2_score_element = np.multiply(np.square(other), self._sensitivity_elements['psi_nu2']) + nu2_score_element = np.multiply(np.square(other), self._sensitivity_elements["psi_nu2"]) nu2 = np.mean(nu2_score_element, axis=0, keepdims=True) psi_nu2 = nu2_score_element - nu2 sensitivity_elements = { - 'sigma2': self._sensitivity_elements['sigma2'], - 'nu2': nu2, - 'psi_sigma2': self._sensitivity_elements['psi_sigma2'], - 'psi_nu2': psi_nu2, - 'riesz_rep': np.multiply(other, self._sensitivity_elements['riesz_rep']), + "sigma2": self._sensitivity_elements["sigma2"], + "nu2": nu2, + "psi_sigma2": self._sensitivity_elements["psi_sigma2"], + "psi_nu2": psi_nu2, + "riesz_rep": np.multiply(other, self._sensitivity_elements["riesz_rep"]), } - doubleml_dict['sensitivity_elements'] = sensitivity_elements + doubleml_dict["sensitivity_elements"] = sensitivity_elements new_obj = DoubleMLFramework(doubleml_dict) else: @@ -447,31 +462,32 @@ def __rmul__(self, other): def _calc_sensitivity_analysis(self, cf_y, cf_d, rho, level): if not self._sensitivity_implemented: - raise NotImplementedError('Sensitivity analysis is not implemented for this model.') + raise NotImplementedError("Sensitivity analysis is not implemented for this model.") # input checks - _check_in_zero_one(cf_y, 'cf_y', include_one=False) - _check_in_zero_one(cf_d, 'cf_d', include_one=False) + _check_in_zero_one(cf_y, "cf_y", include_one=False) + _check_in_zero_one(cf_d, "cf_d", include_one=False) if not isinstance(rho, float): - raise TypeError(f'rho must be of float type. ' - f'{str(rho)} of type {str(type(rho))} was passed.') - _check_in_zero_one(abs(rho), 'The absolute value of rho') - _check_in_zero_one(level, 'The confidence level', include_zero=False, include_one=False) + raise TypeError(f"rho must be of float type. " f"{str(rho)} of type {str(type(rho))} was passed.") + _check_in_zero_one(abs(rho), "The absolute value of rho") + _check_in_zero_one(level, "The confidence level", include_zero=False, include_one=False) # set elements for readability - sigma2 = self.sensitivity_elements['sigma2'] - nu2 = self.sensitivity_elements['nu2'] - psi_sigma = self.sensitivity_elements['psi_sigma2'] - psi_nu = self.sensitivity_elements['psi_nu2'] + sigma2 = self.sensitivity_elements["sigma2"] + nu2 = self.sensitivity_elements["nu2"] + psi_sigma = self.sensitivity_elements["psi_sigma2"] + psi_nu = self.sensitivity_elements["psi_nu2"] psi_scaled = self._scaled_psi if (np.any(sigma2 < 0)) | (np.any(nu2 < 0)): - raise ValueError('sensitivity_elements sigma2 and nu2 have to be positive. ' - f"Got sigma2 {str(sigma2)} and nu2 {str(nu2)}. " - 'Most likely this is due to low quality learners (especially propensity scores).') + raise ValueError( + "sensitivity_elements sigma2 and nu2 have to be positive. " + f"Got sigma2 {str(sigma2)} and nu2 {str(nu2)}. " + "Most likely this is due to low quality learners (especially propensity scores)." + ) # elementwise operations - confounding_strength = np.multiply(np.abs(rho), np.sqrt(np.multiply(cf_y, np.divide(cf_d, 1.0-cf_d)))) + confounding_strength = np.multiply(np.abs(rho), np.sqrt(np.multiply(cf_y, np.divide(cf_d, 1.0 - cf_d)))) sensitivity_scaling = np.sqrt(np.multiply(sigma2, nu2)) # sigma2 and nu2 are of shape (1, n_thetas, n_rep), whereas the all_thetas is of shape (n_thetas, n_rep) @@ -496,25 +512,29 @@ def _calc_sensitivity_analysis(self, cf_y, cf_d, rho, level): smpls_cluster = None n_folds_per_cluster = None else: - smpls = self._cluster_dict['smpls'][i_rep] - cluster_vars = self._cluster_dict['cluster_vars'] - smpls_cluster = self._cluster_dict['smpls_cluster'][i_rep] - n_folds_per_cluster = self._cluster_dict['n_folds_per_cluster'] - - sigma2_lower_hat, _ = _var_est(psi=psi_lower[:, i_theta, i_rep], - psi_deriv=np.ones_like(psi_lower[:, i_theta, i_rep]), - smpls=smpls, - is_cluster_data=self._is_cluster_data, - cluster_vars=cluster_vars, - smpls_cluster=smpls_cluster, - n_folds_per_cluster=n_folds_per_cluster) - sigma2_upper_hat, _ = _var_est(psi=psi_upper[:, i_theta, i_rep], - psi_deriv=np.ones_like(psi_upper[:, i_theta, i_rep]), - smpls=smpls, - is_cluster_data=self._is_cluster_data, - cluster_vars=cluster_vars, - smpls_cluster=smpls_cluster, - n_folds_per_cluster=n_folds_per_cluster) + smpls = self._cluster_dict["smpls"][i_rep] + cluster_vars = self._cluster_dict["cluster_vars"] + smpls_cluster = self._cluster_dict["smpls_cluster"][i_rep] + n_folds_per_cluster = self._cluster_dict["n_folds_per_cluster"] + + sigma2_lower_hat, _ = _var_est( + psi=psi_lower[:, i_theta, i_rep], + psi_deriv=np.ones_like(psi_lower[:, i_theta, i_rep]), + smpls=smpls, + is_cluster_data=self._is_cluster_data, + cluster_vars=cluster_vars, + smpls_cluster=smpls_cluster, + n_folds_per_cluster=n_folds_per_cluster, + ) + sigma2_upper_hat, _ = _var_est( + psi=psi_upper[:, i_theta, i_rep], + psi_deriv=np.ones_like(psi_upper[:, i_theta, i_rep]), + smpls=smpls, + is_cluster_data=self._is_cluster_data, + cluster_vars=cluster_vars, + smpls_cluster=smpls_cluster, + n_folds_per_cluster=n_folds_per_cluster, + ) all_sigma_lower[i_theta, i_rep] = np.sqrt(sigma2_lower_hat) all_sigma_upper[i_theta, i_rep] = np.sqrt(sigma2_upper_hat) @@ -531,38 +551,33 @@ def _calc_sensitivity_analysis(self, cf_y, cf_d, rho, level): ci_lower = np.median(all_ci_lower, axis=1) ci_upper = np.median(all_ci_upper, axis=1) - theta_dict = {'lower': theta_lower, - 'upper': theta_upper} + theta_dict = {"lower": theta_lower, "upper": theta_upper} - se_dict = {'lower': sigma_lower, - 'upper': sigma_upper} + se_dict = {"lower": sigma_lower, "upper": sigma_upper} - ci_dict = {'lower': ci_lower, - 'upper': ci_upper} + ci_dict = {"lower": ci_lower, "upper": ci_upper} - res_dict = {'theta': theta_dict, - 'se': se_dict, - 'ci': ci_dict} + res_dict = {"theta": theta_dict, "se": se_dict, "ci": ci_dict} return res_dict def _calc_robustness_value(self, null_hypothesis, level, rho, idx_treatment): _check_float(null_hypothesis, "null_hypothesis") - _check_integer(idx_treatment, "idx_treatment", lower_bound=0, upper_bound=self._n_thetas-1) + _check_integer(idx_treatment, "idx_treatment", lower_bound=0, upper_bound=self._n_thetas - 1) # check which side is relvant - bound = 'upper' if (null_hypothesis > self.thetas[idx_treatment]) else 'lower' + bound = "upper" if (null_hypothesis > self.thetas[idx_treatment]) else "lower" # minimize the square to find boundary solutions def rv_fct(value, param): - res = self._calc_sensitivity_analysis(cf_y=value, - cf_d=value, - rho=rho, - level=level)[param][bound][idx_treatment] - null_hypothesis + res = ( + self._calc_sensitivity_analysis(cf_y=value, cf_d=value, rho=rho, level=level)[param][bound][idx_treatment] + - null_hypothesis + ) return np.square(res) - rv = minimize_scalar(rv_fct, bounds=(0, 0.9999), method='bounded', args=('theta', )).x - rva = minimize_scalar(rv_fct, bounds=(0, 0.9999), method='bounded', args=('ci', )).x + rv = minimize_scalar(rv_fct, bounds=(0, 0.9999), method="bounded", args=("theta",)).x + rva = minimize_scalar(rv_fct, bounds=(0, 0.9999), method="bounded", args=("ci",)).x return rv, rva @@ -609,12 +624,16 @@ def sensitivity_analysis(self, cf_y=0.03, cf_d=0.03, rho=1.0, level=0.95, null_h if null_hypothesis.shape == (self._n_thetas,): null_hypothesis_vec = null_hypothesis else: - raise ValueError("null_hypothesis is numpy.ndarray but does not have the required " - f"shape ({self._n_thetas},). " - f'Array of shape {str(null_hypothesis.shape)} was passed.') + raise ValueError( + "null_hypothesis is numpy.ndarray but does not have the required " + f"shape ({self._n_thetas},). " + f"Array of shape {str(null_hypothesis.shape)} was passed." + ) else: - raise TypeError("null_hypothesis has to be of type float or np.ndarry. " - f"{str(null_hypothesis)} of type {str(type(null_hypothesis))} was passed.") + raise TypeError( + "null_hypothesis has to be of type float or np.ndarry. " + f"{str(null_hypothesis)} of type {str(type(null_hypothesis))} was passed." + ) # compute sensitivity analysis sensitivity_dict = self._calc_sensitivity_analysis(cf_y=cf_y, cf_d=cf_d, rho=rho, level=level) @@ -625,22 +644,15 @@ def sensitivity_analysis(self, cf_y=0.03, cf_d=0.03, rho=1.0, level=0.95, null_h for i_theta in range(self._n_thetas): rv[i_theta], rva[i_theta] = self._calc_robustness_value( - null_hypothesis=null_hypothesis_vec[i_theta], - level=level, - rho=rho, - idx_treatment=i_theta + null_hypothesis=null_hypothesis_vec[i_theta], level=level, rho=rho, idx_treatment=i_theta ) - sensitivity_dict['rv'] = rv - sensitivity_dict['rva'] = rva + sensitivity_dict["rv"] = rv + sensitivity_dict["rva"] = rva # add all input parameters - input_params = {'cf_y': cf_y, - 'cf_d': cf_d, - 'rho': rho, - 'level': level, - 'null_hypothesis': null_hypothesis_vec} - sensitivity_dict['input'] = input_params + input_params = {"cf_y": cf_y, "cf_d": cf_d, "rho": rho, "level": level, "null_hypothesis": null_hypothesis_vec} + sensitivity_dict["input"] = input_params self._sensitivity_params = sensitivity_dict return self @@ -666,45 +678,40 @@ def confint(self, joint=False, level=0.95): """ if not isinstance(joint, bool): - raise TypeError('joint must be True or False. ' - f'Got {str(joint)}.') + raise TypeError("joint must be True or False. " f"Got {str(joint)}.") if not isinstance(level, float): - raise TypeError('The confidence level must be of float type. ' - f'{str(level)} of type {str(type(level))} was passed.') + raise TypeError( + "The confidence level must be of float type. " f"{str(level)} of type {str(type(level))} was passed." + ) if (level <= 0) | (level >= 1): - raise ValueError('The confidence level must be in (0,1). ' - f'{str(level)} was passed.') + raise ValueError("The confidence level must be in (0,1). " f"{str(level)} was passed.") # compute critical values alpha = 1 - level - percentages = np.array([alpha / 2, 1. - alpha / 2]) + percentages = np.array([alpha / 2, 1.0 - alpha / 2]) if joint: if self._boot_t_stat is None: - raise ValueError('Apply bootstrap() before confint(joint=True).') + raise ValueError("Apply bootstrap() before confint(joint=True).") max_abs_t_value_distribution = np.amax(np.abs(self._boot_t_stat), axis=1) - critical_values = np.quantile( - a=max_abs_t_value_distribution, - q=level, - axis=0) + critical_values = np.quantile(a=max_abs_t_value_distribution, q=level, axis=0) else: critical_values = np.repeat(norm.ppf(percentages[1]), self._n_rep) # compute all cis over repetitions (shape: n_thetas x 2 x n_rep) self._all_cis = np.stack( - (self.all_thetas - self.all_ses * critical_values, - self.all_thetas + self.all_ses * critical_values), - axis=1) + (self.all_thetas - self.all_ses * critical_values, self.all_thetas + self.all_ses * critical_values), axis=1 + ) ci = np.median(self._all_cis, axis=2) - df_ci = pd.DataFrame(ci, columns=['{:.1f} %'.format(i * 100) for i in percentages]) + df_ci = pd.DataFrame(ci, columns=["{:.1f} %".format(i * 100) for i in percentages]) if self._treatment_names is not None: df_ci.set_index(pd.Index(self._treatment_names), inplace=True) return df_ci - def bootstrap(self, method='normal', n_rep_boot=500): + def bootstrap(self, method="normal", n_rep_boot=500): """ Multiplier bootstrap for DoubleMLFrameworks. @@ -724,7 +731,7 @@ def bootstrap(self, method='normal', n_rep_boot=500): _check_bootstrap(method, n_rep_boot) if self._is_cluster_data: - raise NotImplementedError('bootstrap not yet implemented with clustering.') + raise NotImplementedError("bootstrap not yet implemented with clustering.") self._n_rep_boot = n_rep_boot self._boot_method = method @@ -738,7 +745,7 @@ def bootstrap(self, method='normal', n_rep_boot=500): return self - def p_adjust(self, method='romano-wolf'): + def p_adjust(self, method="romano-wolf"): """ Multiple testing adjustment for DoubleML Frameworks. @@ -758,15 +765,16 @@ def p_adjust(self, method='romano-wolf'): A numpy array with all corrected p-values for each repetition. """ if not isinstance(method, str): - raise TypeError('The p_adjust method must be of str type. ' - f'{str(method)} of type {str(type(method))} was passed.') + raise TypeError( + "The p_adjust method must be of str type. " f"{str(method)} of type {str(type(method))} was passed." + ) all_p_vals_corrected = np.full_like(self.all_pvals, np.nan) for i_rep in range(self.n_rep): p_vals_tmp = self.all_pvals[:, i_rep] - if method.lower() in ['rw', 'romano-wolf']: + if method.lower() in ["rw", "romano-wolf"]: if self._boot_t_stat is None: raise ValueError(f'Apply bootstrap() before p_adjust("{method}").') @@ -782,9 +790,7 @@ def p_adjust(self, method='romano-wolf'): ro = np.argsort(stepdown_ind) for i_theta in range(self.n_thetas): - bootstrap_citical_value = np.max( - abs(np.delete(bootstrap_t_stats, stepdown_ind[:i_theta], axis=1)), - axis=1) + bootstrap_citical_value = np.max(abs(np.delete(bootstrap_t_stats, stepdown_ind[:i_theta], axis=1)), axis=1) p_init[i_theta] = np.minimum(1, np.mean(bootstrap_citical_value >= abs_t_stats_tmp[stepdown_ind][i_theta])) for i_theta in range(self.n_thetas): @@ -792,8 +798,8 @@ def p_adjust(self, method='romano-wolf'): p_vals_corrected_tmp_sorted[i_theta] = p_init[i_theta] else: p_vals_corrected_tmp_sorted[i_theta] = np.maximum( - p_init[i_theta], - p_vals_corrected_tmp_sorted[i_theta - 1]) + p_init[i_theta], p_vals_corrected_tmp_sorted[i_theta - 1] + ) # reorder p-values p_vals_corrected_tmp = p_vals_corrected_tmp_sorted[ro] @@ -803,14 +809,23 @@ def p_adjust(self, method='romano-wolf'): all_p_vals_corrected[:, i_rep] = p_vals_corrected_tmp p_vals_corrected = np.median(all_p_vals_corrected, axis=1) - df_p_vals = pd.DataFrame( - np.vstack((self.thetas, p_vals_corrected)).T, - columns=['thetas', 'pval']) + df_p_vals = pd.DataFrame(np.vstack((self.thetas, p_vals_corrected)).T, columns=["thetas", "pval"]) return df_p_vals, all_p_vals_corrected - def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, null_hypothesis=0.0, - include_scenario=True, benchmarks=None, fill=True, grid_bounds=(0.15, 0.15), grid_size=100): + def sensitivity_plot( + self, + idx_treatment=0, + value="theta", + rho=1.0, + level=0.95, + null_hypothesis=0.0, + include_scenario=True, + benchmarks=None, + fill=True, + grid_bounds=(0.15, 0.15), + grid_size=100, + ): """ Contour plot of the sensivity with respect to latent/confounding variables. @@ -863,28 +878,26 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, fig : object Plotly figure of the sensitivity contours. """ - _check_integer(idx_treatment, "idx_treatment", lower_bound=0, upper_bound=self.n_thetas-1) + _check_integer(idx_treatment, "idx_treatment", lower_bound=0, upper_bound=self.n_thetas - 1) if not isinstance(value, str): - raise TypeError('value must be a string. ' - f'{str(value)} of type {type(value)} was passed.') - valid_values = ['theta', 'ci'] + raise TypeError("value must be a string. " f"{str(value)} of type {type(value)} was passed.") + valid_values = ["theta", "ci"] if value not in valid_values: - raise ValueError('Invalid value ' + value + '. ' + - 'Valid values ' + ' or '.join(valid_values) + '.') + raise ValueError("Invalid value " + value + ". " + "Valid values " + " or ".join(valid_values) + ".") _check_float(null_hypothesis, "null_hypothesis") - _check_bool(include_scenario, 'include_scenario') + _check_bool(include_scenario, "include_scenario") if include_scenario and self.sensitivity_params is None: - raise ValueError('Apply sensitivity_analysis() to include senario in sensitivity_plot. ') + raise ValueError("Apply sensitivity_analysis() to include senario in sensitivity_plot. ") _check_benchmarks(benchmarks) - _check_bool(fill, 'fill') + _check_bool(fill, "fill") _check_in_zero_one(grid_bounds[0], "grid_bounds", include_zero=False, include_one=False) _check_in_zero_one(grid_bounds[1], "grid_bounds", include_zero=False, include_one=False) _check_integer(grid_size, "grid_size", lower_bound=10) - null_hypothesis = self.sensitivity_params['input']['null_hypothesis'][idx_treatment] + null_hypothesis = self.sensitivity_params["input"]["null_hypothesis"][idx_treatment] unadjusted_theta = self.thetas[idx_treatment] # check which side is relvant - bound = 'upper' if (null_hypothesis > unadjusted_theta) else 'lower' + bound = "upper" if (null_hypothesis > unadjusted_theta) else "lower" # create evaluation grid cf_d_vec = np.linspace(0, grid_bounds[0], grid_size) @@ -904,12 +917,12 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, contour_values[i_cf_d_grid, i_cf_y_grid] = sens_dict[value][bound][idx_treatment] # get the correct unadjusted value for confidence bands - if value == 'theta': + if value == "theta": unadjusted_value = unadjusted_theta else: - assert value == 'ci' - ci = self.confint(level=self.sensitivity_params['input']['level']) - if bound == 'upper': + assert value == "ci" + ci = self.confint(level=self.sensitivity_params["input"]["level"]) + if bound == "upper": unadjusted_value = ci.iloc[idx_treatment, 1] else: unadjusted_value = ci.iloc[idx_treatment, 0] @@ -917,49 +930,56 @@ def sensitivity_plot(self, idx_treatment=0, value='theta', rho=1.0, level=0.95, # compute the values for the benchmarks benchmark_dict = copy.deepcopy(benchmarks) if benchmarks is not None: - n_benchmarks = len(benchmarks['name']) + n_benchmarks = len(benchmarks["name"]) benchmark_values = np.full(shape=(n_benchmarks,), fill_value=np.nan) - for benchmark_idx in range(len(benchmarks['name'])): + for benchmark_idx in range(len(benchmarks["name"])): sens_dict_bench = self._calc_sensitivity_analysis( - cf_y=benchmarks['cf_y'][benchmark_idx], - cf_d=benchmarks['cf_d'][benchmark_idx], - rho=self.sensitivity_params['input']['rho'], - level=self.sensitivity_params['input']['level'] + cf_y=benchmarks["cf_y"][benchmark_idx], + cf_d=benchmarks["cf_d"][benchmark_idx], + rho=self.sensitivity_params["input"]["rho"], + level=self.sensitivity_params["input"]["level"], ) benchmark_values[benchmark_idx] = sens_dict_bench[value][bound][idx_treatment] - benchmark_dict['value'] = benchmark_values - fig = _sensitivity_contour_plot(x=cf_d_vec, - y=cf_y_vec, - contour_values=contour_values, - unadjusted_value=unadjusted_value, - scenario_x=self.sensitivity_params['input']['cf_d'], - scenario_y=self.sensitivity_params['input']['cf_y'], - scenario_value=self.sensitivity_params[value][bound][idx_treatment], - include_scenario=include_scenario, - benchmarks=benchmark_dict, - fill=fill) + benchmark_dict["value"] = benchmark_values + fig = _sensitivity_contour_plot( + x=cf_d_vec, + y=cf_y_vec, + contour_values=contour_values, + unadjusted_value=unadjusted_value, + scenario_x=self.sensitivity_params["input"]["cf_d"], + scenario_y=self.sensitivity_params["input"]["cf_y"], + scenario_value=self.sensitivity_params[value][bound][idx_treatment], + include_scenario=include_scenario, + benchmarks=benchmark_dict, + fill=fill, + ) return fig def _check_and_set_cluster_data(self, doubleml_dict): self._cluster_dict = None if "is_cluster_data" in doubleml_dict.keys(): - _check_bool(doubleml_dict['is_cluster_data'], 'is_cluster_data') - self._is_cluster_data = doubleml_dict['is_cluster_data'] + _check_bool(doubleml_dict["is_cluster_data"], "is_cluster_data") + self._is_cluster_data = doubleml_dict["is_cluster_data"] if self._is_cluster_data: if "cluster_dict" not in doubleml_dict.keys(): - raise ValueError('If is_cluster_data is True, cluster_dict must be provided.') - - if not isinstance(doubleml_dict['cluster_dict'], dict): - raise TypeError('cluster_dict must be a dictionary.') - - expected_keys_cluster = ['smpls', 'smpls_cluster', 'cluster_vars', 'n_folds_per_cluster'] - if not all(key in doubleml_dict['cluster_dict'].keys() for key in expected_keys_cluster): - raise ValueError('The cluster_dict must contain the following keys: ' + ', '.join(expected_keys_cluster) - + '. Got: ' + ', '.join(doubleml_dict['cluster_dict'].keys()) + '.') + raise ValueError("If is_cluster_data is True, cluster_dict must be provided.") + + if not isinstance(doubleml_dict["cluster_dict"], dict): + raise TypeError("cluster_dict must be a dictionary.") + + expected_keys_cluster = ["smpls", "smpls_cluster", "cluster_vars", "n_folds_per_cluster"] + if not all(key in doubleml_dict["cluster_dict"].keys() for key in expected_keys_cluster): + raise ValueError( + "The cluster_dict must contain the following keys: " + + ", ".join(expected_keys_cluster) + + ". Got: " + + ", ".join(doubleml_dict["cluster_dict"].keys()) + + "." + ) - self._cluster_dict = doubleml_dict['cluster_dict'] + self._cluster_dict = doubleml_dict["cluster_dict"] return @@ -969,25 +989,26 @@ def _check_and_set_sensitivity_elements(self, doubleml_dict): sensitivity_elements = None else: - if not isinstance(doubleml_dict['sensitivity_elements'], dict): - raise TypeError('sensitivity_elements must be a dictionary.') - expected_keys_sensitivity = ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2', 'riesz_rep'] - if not all(key in doubleml_dict['sensitivity_elements'].keys() for key in expected_keys_sensitivity): - raise ValueError('The sensitivity_elements dict must contain the following ' - 'keys: ' + ', '.join(expected_keys_sensitivity)) + if not isinstance(doubleml_dict["sensitivity_elements"], dict): + raise TypeError("sensitivity_elements must be a dictionary.") + expected_keys_sensitivity = ["sigma2", "nu2", "psi_sigma2", "psi_nu2", "riesz_rep"] + if not all(key in doubleml_dict["sensitivity_elements"].keys() for key in expected_keys_sensitivity): + raise ValueError( + "The sensitivity_elements dict must contain the following " "keys: " + ", ".join(expected_keys_sensitivity) + ) for key in expected_keys_sensitivity: - if not isinstance(doubleml_dict['sensitivity_elements'][key], np.ndarray): - raise TypeError(f'The sensitivity element {key} must be a numpy array.') + if not isinstance(doubleml_dict["sensitivity_elements"][key], np.ndarray): + raise TypeError(f"The sensitivity element {key} must be a numpy array.") # set sensitivity elements sensitivity_implemented = True sensitivity_elements = { - 'sigma2': doubleml_dict['sensitivity_elements']['sigma2'], - 'nu2': doubleml_dict['sensitivity_elements']['nu2'], - 'psi_sigma2': doubleml_dict['sensitivity_elements']['psi_sigma2'], - 'psi_nu2': doubleml_dict['sensitivity_elements']['psi_nu2'], - 'riesz_rep': doubleml_dict['sensitivity_elements']['riesz_rep'], + "sigma2": doubleml_dict["sensitivity_elements"]["sigma2"], + "nu2": doubleml_dict["sensitivity_elements"]["nu2"], + "psi_sigma2": doubleml_dict["sensitivity_elements"]["psi_sigma2"], + "psi_nu2": doubleml_dict["sensitivity_elements"]["psi_nu2"], + "riesz_rep": doubleml_dict["sensitivity_elements"]["riesz_rep"], } self._sensitivity_implemented = sensitivity_implemented @@ -1000,49 +1021,70 @@ def _check_framework_shapes(self): score_dim = (self._n_obs, self._n_thetas, self.n_rep) # check if all sizes match if self._thetas.shape != (self._n_thetas,): - raise ValueError(f'The shape of thetas does not match the expected shape ({self._n_thetas},).') + raise ValueError(f"The shape of thetas does not match the expected shape ({self._n_thetas},).") if self._ses.shape != (self._n_thetas,): - raise ValueError(f'The shape of ses does not match the expected shape ({self._n_thetas},).') + raise ValueError(f"The shape of ses does not match the expected shape ({self._n_thetas},).") if self._all_thetas.shape != (self._n_thetas, self._n_rep): - raise ValueError(f'The shape of all_thetas does not match the expected shape ({self._n_thetas}, {self._n_rep}).') + raise ValueError(f"The shape of all_thetas does not match the expected shape ({self._n_thetas}, {self._n_rep}).") if self._all_ses.shape != (self._n_thetas, self._n_rep): - raise ValueError(f'The shape of all_ses does not match the expected shape ({self._n_thetas}, {self._n_rep}).') + raise ValueError(f"The shape of all_ses does not match the expected shape ({self._n_thetas}, {self._n_rep}).") if self._var_scaling_factors.shape != (self._n_thetas,): - raise ValueError(f'The shape of var_scaling_factors does not match the expected shape ({self._n_thetas},).') + raise ValueError(f"The shape of var_scaling_factors does not match the expected shape ({self._n_thetas},).") # dimension of scaled_psi is n_obs x n_thetas x n_rep (per default) if self._scaled_psi.shape != score_dim: - raise ValueError(('The shape of scaled_psi does not match the expected ' - f'shape ({self._n_obs}, {self._n_thetas}, {self._n_rep}).')) + raise ValueError( + ( + "The shape of scaled_psi does not match the expected " + f"shape ({self._n_obs}, {self._n_thetas}, {self._n_rep})." + ) + ) if self._sensitivity_implemented: - if self._sensitivity_elements['sigma2'].shape != (1, self._n_thetas, self.n_rep): - raise ValueError('The shape of sigma2 does not match the expected shape ' - f'(1, {self._n_thetas}, {self._n_rep}).') - if self._sensitivity_elements['nu2'].shape != (1, self._n_thetas, self.n_rep): - raise ValueError(f'The shape of nu2 does not match the expected shape (1, {self._n_thetas}, {self._n_rep}).') - if self._sensitivity_elements['psi_sigma2'].shape != score_dim: - raise ValueError(('The shape of psi_sigma2 does not match the expected ' - f'shape ({self._n_obs}, {self._n_thetas}, {self._n_rep}).')) - if self._sensitivity_elements['psi_nu2'].shape != score_dim: - raise ValueError(('The shape of psi_nu2 does not match the expected ' - f'shape ({self._n_obs}, {self._n_thetas}, {self._n_rep}).')) - if self._sensitivity_elements['riesz_rep'].shape != score_dim: - raise ValueError(('The shape of riesz_rep does not match the expected ' - f'shape ({self._n_obs}, {self._n_thetas}, {self._n_rep}).')) + if self._sensitivity_elements["sigma2"].shape != (1, self._n_thetas, self.n_rep): + raise ValueError( + "The shape of sigma2 does not match the expected shape " f"(1, {self._n_thetas}, {self._n_rep})." + ) + if self._sensitivity_elements["nu2"].shape != (1, self._n_thetas, self.n_rep): + raise ValueError(f"The shape of nu2 does not match the expected shape (1, {self._n_thetas}, {self._n_rep}).") + if self._sensitivity_elements["psi_sigma2"].shape != score_dim: + raise ValueError( + ( + "The shape of psi_sigma2 does not match the expected " + f"shape ({self._n_obs}, {self._n_thetas}, {self._n_rep})." + ) + ) + if self._sensitivity_elements["psi_nu2"].shape != score_dim: + raise ValueError( + ( + "The shape of psi_nu2 does not match the expected " + f"shape ({self._n_obs}, {self._n_thetas}, {self._n_rep})." + ) + ) + if self._sensitivity_elements["riesz_rep"].shape != score_dim: + raise ValueError( + ( + "The shape of riesz_rep does not match the expected " + f"shape ({self._n_obs}, {self._n_thetas}, {self._n_rep})." + ) + ) return None def _check_treatment_names(self, treatment_names): if not isinstance(treatment_names, list): - raise TypeError('treatment_names must be a list. ' - f'Got {str(treatment_names)} of type {str(type(treatment_names))}.') + raise TypeError( + "treatment_names must be a list. " f"Got {str(treatment_names)} of type {str(type(treatment_names))}." + ) is_str = [isinstance(name, str) for name in treatment_names] if not all(is_str): - raise TypeError('treatment_names must be a list of strings. ' - f'At least one element is not a string: {str(treatment_names)}.') + raise TypeError( + "treatment_names must be a list of strings. " f"At least one element is not a string: {str(treatment_names)}." + ) if len(treatment_names) != self._n_thetas: - raise ValueError('The length of treatment_names does not match the number of treatments. ' - f'Got {self._n_thetas} treatments and {len(treatment_names)} treatment names.') + raise ValueError( + "The length of treatment_names does not match the number of treatments. " + f"Got {self._n_thetas} treatments and {len(treatment_names)} treatment names." + ) return None @@ -1051,10 +1093,10 @@ def concat(objs): Concatenate DoubleMLFramework objects. """ if len(objs) == 0: - raise TypeError('Need at least one object to concatenate.') + raise TypeError("Need at least one object to concatenate.") if not all(isinstance(obj, DoubleMLFramework) for obj in objs): - raise TypeError('All objects must be of type DoubleMLFramework.') + raise TypeError("All objects must be of type DoubleMLFramework.") # check on internal consitency of objects _ = [obj._check_framework_shapes() for obj in objs] @@ -1070,27 +1112,27 @@ def concat(objs): ses = np.concatenate([obj.ses for obj in objs], axis=0) if any(obj._is_cluster_data for obj in objs): - raise NotImplementedError('concat not yet implemented with clustering.') + raise NotImplementedError("concat not yet implemented with clustering.") else: is_cluster_data = False doubleml_dict = { - 'thetas': thetas, - 'ses': ses, - 'all_thetas': all_thetas, - 'all_ses': all_ses, - 'var_scaling_factors': var_scaling_factors, - 'scaled_psi': scaled_psi, - 'is_cluster_data': is_cluster_data, + "thetas": thetas, + "ses": ses, + "all_thetas": all_thetas, + "all_ses": all_ses, + "var_scaling_factors": var_scaling_factors, + "scaled_psi": scaled_psi, + "is_cluster_data": is_cluster_data, } if all(obj._sensitivity_implemented for obj in objs): sensitivity_elements = {} - for key in ['sigma2', 'nu2', 'psi_sigma2', 'psi_nu2', 'riesz_rep']: + for key in ["sigma2", "nu2", "psi_sigma2", "psi_nu2", "riesz_rep"]: assert all(key in obj._sensitivity_elements.keys() for obj in objs) sensitivity_elements[key] = np.concatenate([obj._sensitivity_elements[key] for obj in objs], axis=1) - doubleml_dict['sensitivity_elements'] = sensitivity_elements + doubleml_dict["sensitivity_elements"] = sensitivity_elements new_obj = DoubleMLFramework(doubleml_dict) diff --git a/doubleml/double_ml_score_mixins.py b/doubleml/double_ml_score_mixins.py index 7ac4cbf42..4615b8c77 100644 --- a/doubleml/double_ml_score_mixins.py +++ b/doubleml/double_ml_score_mixins.py @@ -25,36 +25,37 @@ class LinearScoreMixin: `score functions `_ and on `variance estimation `_ in the DoubleML user guide. """ - _score_type = 'linear' + + _score_type = "linear" @property def _score_element_names(self): - return ['psi_a', 'psi_b'] + return ["psi_a", "psi_b"] def _compute_score(self, psi_elements, coef): - psi = psi_elements['psi_a'] * coef + psi_elements['psi_b'] + psi = psi_elements["psi_a"] * coef + psi_elements["psi_b"] return psi def _compute_score_deriv(self, psi_elements, coef): - return psi_elements['psi_a'] + return psi_elements["psi_a"] def _est_coef(self, psi_elements, smpls=None, scaling_factor=None, inds=None): - psi_a = psi_elements['psi_a'] - psi_b = psi_elements['psi_b'] + psi_a = psi_elements["psi_a"] + psi_b = psi_elements["psi_b"] if inds is not None: psi_a = psi_a[inds] psi_b = psi_b[inds] if not self._is_cluster_data: - coef = - np.mean(psi_b) / np.mean(psi_a) + coef = -np.mean(psi_b) / np.mean(psi_a) # for cluster we need the smpls and the scaling factors else: assert smpls is not None assert scaling_factor is not None assert inds is None # if we have clustered data and dml2 the solution is the root of a weighted sum - psi_a_subsample_mean = 0. - psi_b_subsample_mean = 0. + psi_a_subsample_mean = 0.0 + psi_b_subsample_mean = 0.0 for i_fold, (_, test_index) in enumerate(smpls): psi_a_subsample_mean += scaling_factor[i_fold] * np.sum(psi_a[test_index]) psi_b_subsample_mean += scaling_factor[i_fold] * np.sum(psi_b[test_index]) @@ -81,7 +82,8 @@ class NonLinearScoreMixin: ``_compute_score_deriv``, which should implement the evaluation of the derivative of the score function :math:`\\frac{\\partial}{\\partial \\theta} \\psi(W; \\theta, \\eta)`, need to be added model-specifically. """ - _score_type = 'nonlinear' + + _score_type = "nonlinear" _coef_start_val = np.nan _coef_bounds = None @@ -119,7 +121,7 @@ def _aggregate_obs(psi): # if we have clustered data the solution is the root of a weighted sum else: - psi_mean = 0. + psi_mean = 0.0 for i_fold, (_, test_index) in enumerate(smpls): psi_mean += scaling_factor[i_fold] * np.sum(psi[test_index]) @@ -143,71 +145,68 @@ def score_deriv(theta): bounded = (self._coef_bounds[0] > -np.inf) & (self._coef_bounds[1] < np.inf) if not bounded: - root_res = root_scalar(score, - x0=self._coef_start_val, - fprime=score_deriv, - method='newton') + root_res = root_scalar(score, x0=self._coef_start_val, fprime=score_deriv, method="newton") theta_hat = root_res.root if not root_res.converged: score_val = score(theta_hat) - warnings.warn('Could not find a root of the score function.\n ' - f'Flag: {root_res.flag}.\n' - f'Score value found is {score_val} ' - f'for parameter theta equal to {theta_hat}.') + warnings.warn( + "Could not find a root of the score function.\n " + f"Flag: {root_res.flag}.\n" + f"Score value found is {score_val} " + f"for parameter theta equal to {theta_hat}." + ) else: signs_different, bracket_guess = _get_bracket_guess(score, self._coef_start_val, self._coef_bounds) if signs_different: - root_res = root_scalar(score, - bracket=bracket_guess, - method='brentq') + root_res = root_scalar(score, bracket=bracket_guess, method="brentq") theta_hat = root_res.root else: # try to find an alternative start value def score_squared(theta): res = np.power(np.mean(self._compute_score(psi_elements, theta)), 2) return res + # def score_squared_deriv(theta, inds): # res = 2 * np.mean(self._compute_score(psi_elements, theta, inds)) * \ # np.mean(self._compute_score_deriv(psi_elements, theta, inds)) # return res - alt_coef_start, _, _ = fmin_l_bfgs_b(score_squared, - self._coef_start_val, - approx_grad=True, - bounds=[self._coef_bounds]) + alt_coef_start, _, _ = fmin_l_bfgs_b( + score_squared, self._coef_start_val, approx_grad=True, bounds=[self._coef_bounds] + ) signs_different, bracket_guess = _get_bracket_guess(score, alt_coef_start, self._coef_bounds) if signs_different: - root_res = root_scalar(score, - bracket=bracket_guess, - method='brentq') + root_res = root_scalar(score, bracket=bracket_guess, method="brentq") theta_hat = root_res.root else: score_val_sign = np.sign(score(alt_coef_start)) if score_val_sign > 0: theta_hat_array, score_val, _ = fmin_l_bfgs_b( - score, - self._coef_start_val, - approx_grad=True, - bounds=[self._coef_bounds]) + score, self._coef_start_val, approx_grad=True, bounds=[self._coef_bounds] + ) theta_hat = theta_hat_array.item() - warnings.warn('Could not find a root of the score function.\n ' - f'Minimum score value found is {score_val} ' - f'for parameter theta equal to {theta_hat}.\n ' - 'No theta found such that the score function evaluates to a negative value.') + warnings.warn( + "Could not find a root of the score function.\n " + f"Minimum score value found is {score_val} " + f"for parameter theta equal to {theta_hat}.\n " + "No theta found such that the score function evaluates to a negative value." + ) else: + def neg_score(theta): - res = - np.mean(self._compute_score(psi_elements, theta)) + res = -np.mean(self._compute_score(psi_elements, theta)) return res + theta_hat_array, neg_score_val, _ = fmin_l_bfgs_b( - neg_score, - self._coef_start_val, - approx_grad=True, - bounds=[self._coef_bounds]) + neg_score, self._coef_start_val, approx_grad=True, bounds=[self._coef_bounds] + ) theta_hat = theta_hat_array.item() - warnings.warn('Could not find a root of the score function. ' - f'Maximum score value found is {-1*neg_score_val} ' - f'for parameter theta equal to {theta_hat}. ' - 'No theta found such that the score function evaluates to a positive value.') + warnings.warn( + "Could not find a root of the score function. " + f"Maximum score value found is {-1*neg_score_val} " + f"for parameter theta equal to {theta_hat}. " + "No theta found such that the score function evaluates to a positive value." + ) return theta_hat From f95441b1c3288a2812b4f982fdebeab098925efe Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:48:45 +0100 Subject: [PATCH 18/28] ruff conf.py --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index 9570f7aef..0c6e3930e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -12,6 +12,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath('..')) From 0010aaaf8736871d59a10ca42dc6ad01b5a61c52 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:49:29 +0100 Subject: [PATCH 19/28] format conf.py --- doc/conf.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 0c6e3930e..b4ce1c05b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,17 +13,17 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- Project information ----------------------------------------------------- -project = 'DoubleML' -copyright = '2021, Bach, P., Chernozhukov, V., Klaassen, S., Kurz, M. S., and Spindler, M.' -author = 'Bach, P., Chernozhukov, V., Klaassen, S., Kurz, M. S., and Spindler, M.' +project = "DoubleML" +copyright = "2021, Bach, P., Chernozhukov, V., Klaassen, S., Kurz, M. S., and Spindler, M." +author = "Bach, P., Chernozhukov, V., Klaassen, S., Kurz, M. S., and Spindler, M." # The full version, including alpha/beta/rc tags -release = '0.10.dev0' +release = "0.10.dev0" # -- General configuration --------------------------------------------------- @@ -32,25 +32,25 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.napoleon', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -master_doc = 'index' +master_doc = "index" -autoclass_content = 'class' +autoclass_content = "class" autosummary_generate = True # -- Options for HTML output ------------------------------------------------- @@ -58,12 +58,12 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'pydata_sphinx_theme' +html_theme = "pydata_sphinx_theme" html_theme_options = { - 'github_url': 'https://github.com/DoubleML/doubleml-for-py', - 'navigation_with_keys': False, - 'show_toc_level': 0 + "github_url": "https://github.com/DoubleML/doubleml-for-py", + "navigation_with_keys": False, + "show_toc_level": 0, } # Add any paths that contain custom static files (such as style sheets) here, @@ -75,9 +75,9 @@ # intersphinx configuration intersphinx_mapping = { - 'python': ('https://docs.python.org/{.major}'.format(sys.version_info), None), - 'sklearn': ('https://scikit-learn.org/stable/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), - 'statsmodels': ('https://www.statsmodels.org/stable/', None), + "python": ("https://docs.python.org/{.major}".format(sys.version_info), None), + "sklearn": ("https://scikit-learn.org/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "statsmodels": ("https://www.statsmodels.org/stable/", None), } From e23134bb83186abb62d21d3fa6da998f4f185beb Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 11:52:04 +0100 Subject: [PATCH 20/28] reformat with ruff --- doubleml/datasets.py | 2 +- doubleml/did/did_cs.py | 1 - doubleml/did/tests/_utils_did_cs_manual.py | 3 - doubleml/did/tests/_utils_did_manual.py | 3 - doubleml/double_ml.py | 36 ++++--- doubleml/double_ml_data.py | 69 +++++++------- doubleml/double_ml_framework.py | 36 +++---- doubleml/double_ml_score_mixins.py | 2 +- doubleml/irm/apo.py | 2 +- doubleml/irm/apos.py | 6 +- doubleml/irm/cvar.py | 2 +- doubleml/irm/iivm.py | 6 +- doubleml/irm/irm.py | 7 +- doubleml/irm/lpq.py | 4 +- doubleml/irm/pq.py | 4 +- doubleml/irm/qte.py | 7 +- doubleml/irm/ssm.py | 2 +- doubleml/irm/tests/_utils_apo_manual.py | 1 - doubleml/irm/tests/_utils_irm_manual.py | 3 - doubleml/irm/tests/_utils_lpq_manual.py | 1 - doubleml/irm/tests/_utils_qte_manual.py | 1 - doubleml/irm/tests/_utils_ssm_manual.py | 1 - .../tests/test_apo_external_predictions.py | 1 - doubleml/irm/tests/test_ssm_exceptions.py | 2 +- doubleml/plm/pliv.py | 8 +- doubleml/plm/plr.py | 6 +- doubleml/rdd/rdd.py | 24 ++--- doubleml/rdd/tests/conftest.py | 2 +- doubleml/rdd/tests/test_rdd_exceptions.py | 1 - .../_utils_doubleml_sensitivity_manual.py | 1 - doubleml/tests/test_dml_data.py | 4 +- doubleml/tests/test_exceptions.py | 16 +--- doubleml/tests/test_nonlinear_score_mixin.py | 2 +- doubleml/tests/test_return_types.py | 1 - doubleml/tests/test_sensitivity.py | 1 - doubleml/tests/test_set_sample_splitting.py | 9 +- doubleml/utils/_checks.py | 93 ++++++++----------- doubleml/utils/_estimation.py | 1 - doubleml/utils/_plots.py | 1 - doubleml/utils/blp.py | 19 ++-- doubleml/utils/gain_statistics.py | 8 +- doubleml/utils/global_learner.py | 2 - doubleml/utils/policytree.py | 14 ++- doubleml/utils/resampling.py | 3 +- doubleml/utils/tests/test_global_learners.py | 1 - .../tests/test_var_est_and_aggregation.py | 1 - 46 files changed, 169 insertions(+), 251 deletions(-) diff --git a/doubleml/datasets.py b/doubleml/datasets.py index 909db74a4..8be9cd4ef 100644 --- a/doubleml/datasets.py +++ b/doubleml/datasets.py @@ -1207,7 +1207,7 @@ def f_ps(w, xi): m_short = np.clip(m_short, trimming_threshold, 1.0 - trimming_threshold) warnings.warn( f"Propensity score is close to 0 or 1. " - f"Trimming is at {trimming_threshold} and {1.0-trimming_threshold} is applied" + f"Trimming is at {trimming_threshold} and {1.0 - trimming_threshold} is applied" ) # generate treatment based on long form u = np.random.uniform(low=0, high=1, size=n_obs) diff --git a/doubleml/did/did_cs.py b/doubleml/did/did_cs.py index f223cccf5..2b7b5b12c 100644 --- a/doubleml/did/did_cs.py +++ b/doubleml/did/did_cs.py @@ -353,7 +353,6 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa return psi_elements, preds def _score_elements(self, y, d, t, g_hat_d0_t0, g_hat_d0_t1, g_hat_d1_t0, g_hat_d1_t1, m_hat, p_hat, lambda_hat): - # calculate residuals resid_d0_t0 = y - g_hat_d0_t0 resid_d0_t1 = y - g_hat_d0_t1 diff --git a/doubleml/did/tests/_utils_did_cs_manual.py b/doubleml/did/tests/_utils_did_cs_manual.py index 0c1ac24b9..f14a52a08 100644 --- a/doubleml/did/tests/_utils_did_cs_manual.py +++ b/doubleml/did/tests/_utils_did_cs_manual.py @@ -243,7 +243,6 @@ def did_cs_score_elements( score, in_sample_normalization, ): - if score == "observational": if in_sample_normalization: weight_psi_a = np.divide(d, np.mean(d)) @@ -324,7 +323,6 @@ def did_cs_score_elements( def tune_nuisance_did_cs(y, x, d, t, ml_g, ml_m, smpls, score, n_folds_tune, param_grid_g, param_grid_m): - smpls_d0_t0 = np.intersect1d(np.where(d == 0)[0], np.where(t == 0)[0]) smpls_d0_t1 = np.intersect1d(np.where(d == 0)[0], np.where(t == 1)[0]) smpls_d1_t0 = np.intersect1d(np.where(d == 1)[0], np.where(t == 0)[0]) @@ -360,7 +358,6 @@ def fit_sensitivity_elements_did_cs(y, d, t, all_coef, predictions, score, in_sa psi_nu2 = np.full(shape=(n_obs, n_rep, n_treat), fill_value=np.nan) for i_rep in range(n_rep): - g_hat_d0_t0 = predictions["ml_g_d0_t0"][:, i_rep, 0] g_hat_d0_t1 = predictions["ml_g_d0_t1"][:, i_rep, 0] g_hat_d1_t0 = predictions["ml_g_d1_t0"][:, i_rep, 0] diff --git a/doubleml/did/tests/_utils_did_manual.py b/doubleml/did/tests/_utils_did_manual.py index 067795bb6..e48c90423 100644 --- a/doubleml/did/tests/_utils_did_manual.py +++ b/doubleml/did/tests/_utils_did_manual.py @@ -135,7 +135,6 @@ def did_dml2(psi_a, psi_b): def did_score_elements(g_hat0, g_hat1, m_hat, p_hat, resid_d0, d, score, in_sample_normalization): - if score == "observational": if in_sample_normalization: weight_psi_a = np.divide(d, np.mean(d)) @@ -195,7 +194,6 @@ def boot_did(y, thetas, ses, all_psi_a, all_psi_b, all_smpls, bootstrap, n_rep_b def boot_did_single_split(theta, psi_a, psi_b, smpls, se, weights, n_rep_boot, apply_cross_fitting): - if apply_cross_fitting: J = np.mean(psi_a) else: @@ -237,7 +235,6 @@ def fit_sensitivity_elements_did(y, d, all_coef, predictions, score, in_sample_n psi_nu2 = np.full(shape=(n_obs, n_rep, n_treat), fill_value=np.nan) for i_rep in range(n_rep): - g_hat0 = predictions["ml_g0"][:, i_rep, 0] g_hat1 = predictions["ml_g1"][:, i_rep, 0] diff --git a/doubleml/double_ml.py b/doubleml/double_ml.py index 59e496c10..e83a9b786 100644 --- a/doubleml/double_ml.py +++ b/doubleml/double_ml.py @@ -61,10 +61,10 @@ def __init__(self, obj_dml_data, n_folds, n_rep, score, draw_sample_splitting): # check resampling specifications if not isinstance(n_folds, int): raise TypeError( - "The number of folds must be of int type. " f"{str(n_folds)} of type {str(type(n_folds))} was passed." + f"The number of folds must be of int type. {str(n_folds)} of type {str(type(n_folds))} was passed." ) if n_folds < 1: - raise ValueError("The number of folds must be positive. " f"{str(n_folds)} was passed.") + raise ValueError(f"The number of folds must be positive. {str(n_folds)} was passed.") if not isinstance(n_rep, int): raise TypeError( @@ -72,12 +72,10 @@ def __init__(self, obj_dml_data, n_folds, n_rep, score, draw_sample_splitting): f"{str(n_rep)} of type {str(type(n_rep))} was passed." ) if n_rep < 1: - raise ValueError( - "The number of repetitions for the sample splitting must be positive. " f"{str(n_rep)} was passed." - ) + raise ValueError(f"The number of repetitions for the sample splitting must be positive. {str(n_rep)} was passed.") if not isinstance(draw_sample_splitting, bool): - raise TypeError("draw_sample_splitting must be True or False. " f"Got {str(draw_sample_splitting)}.") + raise TypeError(f"draw_sample_splitting must be True or False. Got {str(draw_sample_splitting)}.") # set resampling specifications if self._is_cluster_data: @@ -140,7 +138,7 @@ def __str__(self): f"No. repeated sample splits: {self.n_rep}\n" ) else: - resampling_info = f"No. folds: {self.n_folds}\n" f"No. repeated sample splits: {self.n_rep}\n" + resampling_info = f"No. folds: {self.n_folds}\nNo. repeated sample splits: {self.n_rep}\n" fit_summary = str(self.summary) res = ( header @@ -757,7 +755,7 @@ def tune( scoring_methods[learner] = None if not isinstance(tune_on_folds, bool): - raise TypeError("tune_on_folds must be True or False. " f"Got {str(tune_on_folds)}.") + raise TypeError(f"tune_on_folds must be True or False. Got {str(tune_on_folds)}.") if not isinstance(n_folds_tune, int): raise TypeError( @@ -765,10 +763,10 @@ def tune( f"{str(n_folds_tune)} of type {str(type(n_folds_tune))} was passed." ) if n_folds_tune < 2: - raise ValueError("The number of folds used for tuning must be at least two. " f"{str(n_folds_tune)} was passed.") + raise ValueError(f"The number of folds used for tuning must be at least two. {str(n_folds_tune)} was passed.") if (not isinstance(search_mode, str)) | (search_mode not in ["grid_search", "randomized_search"]): - raise ValueError('search_mode must be "grid_search" or "randomized_search". ' f"Got {str(search_mode)}.") + raise ValueError(f'search_mode must be "grid_search" or "randomized_search". Got {str(search_mode)}.') if not isinstance(n_iter_randomized_search, int): raise TypeError( @@ -790,10 +788,10 @@ def tune( ) if not isinstance(set_as_params, bool): - raise TypeError("set_as_params must be True or False. " f"Got {str(set_as_params)}.") + raise TypeError(f"set_as_params must be True or False. Got {str(set_as_params)}.") if not isinstance(return_tune_res, bool): - raise TypeError("return_tune_res must be True or False. " f"Got {str(return_tune_res)}.") + raise TypeError(f"return_tune_res must be True or False. Got {str(return_tune_res)}.") if tune_on_folds: tuning_res = [[None] * self.n_rep] * self._dml_data.n_treat @@ -969,10 +967,10 @@ def _check_fit(self, n_jobs_cv, store_predictions, external_predictions, store_m ) if not isinstance(store_predictions, bool): - raise TypeError("store_predictions must be True or False. " f"Got {str(store_predictions)}.") + raise TypeError(f"store_predictions must be True or False. Got {str(store_predictions)}.") if not isinstance(store_models, bool): - raise TypeError("store_models must be True or False. " f"Got {str(store_models)}.") + raise TypeError(f"store_models must be True or False. Got {str(store_models)}.") # check if external predictions are implemented if self._external_predictions_implemented: @@ -1169,7 +1167,7 @@ def evaluate_learners(self, learners=None, metric=_rmse): # check metric if not callable(metric): - raise TypeError("metric should be a callable. " "%r was passed." % metric) + raise TypeError("metric should be a callable. %r was passed." % metric) if all(learner in self.params_names for learner in learners): if self.nuisance_targets is None: @@ -1189,7 +1187,7 @@ def evaluate_learners(self, learners=None, metric=_rmse): return dist else: raise ValueError( - f"The learners have to be a subset of {str(self.params_names)}. " f"Learners {str(learners)} provided." + f"The learners have to be a subset of {str(self.params_names)}. Learners {str(learners)} provided." ) def draw_sample_splitting(self): @@ -1389,7 +1387,7 @@ def _get_score_elements(self, i_rep, i_treat): def _set_score_elements(self, psi_elements, i_rep, i_treat): if not isinstance(psi_elements, dict): raise TypeError( - "_ml_nuisance_and_score_elements must return score elements in a dict. " f"Got type {str(type(psi_elements))}." + f"_ml_nuisance_and_score_elements must return score elements in a dict. Got type {str(type(psi_elements))}." ) if not (set(self._score_element_names) == set(psi_elements.keys())): raise ValueError( @@ -1602,7 +1600,7 @@ def sensitivity_benchmark(self, benchmarking_set, fit_args=None): raise NotImplementedError(f"Sensitivity analysis not yet implemented for {self.__class__.__name__}.") if not isinstance(benchmarking_set, list): raise TypeError( - "benchmarking_set must be a list. " f"{str(benchmarking_set)} of type {type(benchmarking_set)} was passed." + f"benchmarking_set must be a list. {str(benchmarking_set)} of type {type(benchmarking_set)} was passed." ) if len(benchmarking_set) == 0: raise ValueError("benchmarking_set must not be empty.") @@ -1612,7 +1610,7 @@ def sensitivity_benchmark(self, benchmarking_set, fit_args=None): f"{str(benchmarking_set)} was passed." ) if fit_args is not None and not isinstance(fit_args, dict): - raise TypeError("fit_args must be a dict. " f"{str(fit_args)} of type {type(fit_args)} was passed.") + raise TypeError(f"fit_args must be a dict. {str(fit_args)} of type {type(fit_args)} was passed.") # refit short form of the model x_list_short = [x for x in x_list_long if x not in benchmarking_set] diff --git a/doubleml/double_ml_data.py b/doubleml/double_ml_data.py index 552e7e795..3ebf2f765 100644 --- a/doubleml/double_ml_data.py +++ b/doubleml/double_ml_data.py @@ -16,9 +16,9 @@ class DoubleMLBaseData(ABC): def __init__(self, data): if not isinstance(data, pd.DataFrame): - raise TypeError("data must be of pd.DataFrame type. " f"{str(data)} of type {str(type(data))} was passed.") + raise TypeError(f"data must be of pd.DataFrame type. {str(data)} of type {str(type(data))} was passed.") if not data.columns.is_unique: - raise ValueError("Invalid pd.DataFrame: " "Contains duplicate column names.") + raise ValueError("Invalid pd.DataFrame: Contains duplicate column names.") self._data = data def __str__(self): @@ -292,9 +292,9 @@ def from_arrays(cls, x, y, d, z=None, t=None, s=None, use_other_treat_as_covaria if d.shape[1] == 1: d_cols = ["d"] else: - d_cols = [f"d{i+1}" for i in np.arange(d.shape[1])] + d_cols = [f"d{i + 1}" for i in np.arange(d.shape[1])] - x_cols = [f"X{i+1}" for i in np.arange(x.shape[1])] + x_cols = [f"X{i + 1}" for i in np.arange(x.shape[1])] # basline version with features, outcome and treatments data = pd.DataFrame(np.column_stack((x, y, d)), columns=x_cols + [y_col] + d_cols) @@ -426,9 +426,9 @@ def x_cols(self, value): f"{str(value)} of type {str(type(value))} was passed." ) if not len(set(value)) == len(value): - raise ValueError("Invalid covariates x_cols: " "Contains duplicate values.") + raise ValueError("Invalid covariates x_cols: Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError("Invalid covariates x_cols. " "At least one covariate is no data column.") + raise ValueError("Invalid covariates x_cols. At least one covariate is no data column.") assert set(value).issubset(set(self.all_variables)) self._x_cols = value else: @@ -462,9 +462,9 @@ def d_cols(self, value): f"{str(value)} of type {str(type(value))} was passed." ) if not len(set(value)) == len(value): - raise ValueError("Invalid treatment variable(s) d_cols: " "Contains duplicate values.") + raise ValueError("Invalid treatment variable(s) d_cols: Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError("Invalid treatment variable(s) d_cols. " "At least one treatment variable is no data column.") + raise ValueError("Invalid treatment variable(s) d_cols. At least one treatment variable is no data column.") self._d_cols = value if reset_value: self._check_disjoint_sets() @@ -483,10 +483,10 @@ def y_col(self, value): reset_value = hasattr(self, "_y_col") if not isinstance(value, str): raise TypeError( - "The outcome variable y_col must be of str type. " f"{str(value)} of type {str(type(value))} was passed." + f"The outcome variable y_col must be of str type. {str(value)} of type {str(type(value))} was passed." ) if value not in self.all_variables: - raise ValueError("Invalid outcome variable y_col. " f"{value} is no data column.") + raise ValueError(f"Invalid outcome variable y_col. {value} is no data column.") self._y_col = value if reset_value: self._check_disjoint_sets() @@ -511,10 +511,10 @@ def z_cols(self, value): f"{str(value)} of type {str(type(value))} was passed." ) if not len(set(value)) == len(value): - raise ValueError("Invalid instrumental variable(s) z_cols: " "Contains duplicate values.") + raise ValueError("Invalid instrumental variable(s) z_cols: Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): raise ValueError( - "Invalid instrumental variable(s) z_cols. " "At least one instrumental variable is no data column." + "Invalid instrumental variable(s) z_cols. At least one instrumental variable is no data column." ) self._z_cols = value else: @@ -540,7 +540,7 @@ def t_col(self, value): f"{str(value)} of type {str(type(value))} was passed." ) if value not in self.all_variables: - raise ValueError("Invalid time variable t_col. " f"{value} is no data column.") + raise ValueError(f"Invalid time variable t_col. {value} is no data column.") self._t_col = value else: self._t_col = None @@ -565,7 +565,7 @@ def s_col(self, value): f"{str(value)} of type {str(type(value))} was passed." ) if value not in self.all_variables: - raise ValueError("Invalid score or selection variable s_col. " f"{value} is no data column.") + raise ValueError(f"Invalid score or selection variable s_col. {value} is no data column.") self._s_col = value else: self._s_col = None @@ -584,7 +584,7 @@ def use_other_treat_as_covariate(self): def use_other_treat_as_covariate(self, value): reset_value = hasattr(self, "_use_other_treat_as_covariate") if not isinstance(value, bool): - raise TypeError("use_other_treat_as_covariate must be True or False. " f"Got {str(value)}.") + raise TypeError(f"use_other_treat_as_covariate must be True or False. Got {str(value)}.") self._use_other_treat_as_covariate = value if reset_value: # by default, we initialize to the first treatment variable @@ -644,10 +644,10 @@ def set_x_d(self, treatment_var): """ if not isinstance(treatment_var, str): raise TypeError( - "treatment_var must be of str type. " f"{str(treatment_var)} of type {str(type(treatment_var))} was passed." + f"treatment_var must be of str type. {str(treatment_var)} of type {str(type(treatment_var))} was passed." ) if treatment_var not in self.d_cols: - raise ValueError("Invalid treatment_var. " f"{treatment_var} is not in d_cols.") + raise ValueError(f"Invalid treatment_var. {treatment_var} is not in d_cols.") if self.use_other_treat_as_covariate: # note that the following line needs to be adapted in case an intersection of x_cols and d_cols as allowed # (see https://github.com/DoubleML/doubleml-for-py/issues/83) @@ -687,10 +687,10 @@ def _check_disjoint_sets_y_d_x_z_t_s(self): d_cols_set = set(self.d_cols) if not y_col_set.isdisjoint(x_cols_set): - raise ValueError(f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and covariate in " "``x_cols``.") + raise ValueError(f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and covariate in ``x_cols``.") if not y_col_set.isdisjoint(d_cols_set): raise ValueError( - f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and treatment variable in " "``d_cols``." + f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and treatment variable in ``d_cols``." ) # note that the line xd_list = self.x_cols + self.d_cols in method set_x_d needs adaption if an intersection of # x_cols and d_cols as allowed (see https://github.com/DoubleML/doubleml-for-py/issues/83) @@ -704,8 +704,7 @@ def _check_disjoint_sets_y_d_x_z_t_s(self): z_cols_set = set(self.z_cols) if not y_col_set.isdisjoint(z_cols_set): raise ValueError( - f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and instrumental " - "variable in ``z_cols``." + f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and instrumental variable in ``z_cols``." ) if not d_cols_set.isdisjoint(z_cols_set): raise ValueError( @@ -714,7 +713,7 @@ def _check_disjoint_sets_y_d_x_z_t_s(self): ) if not x_cols_set.isdisjoint(z_cols_set): raise ValueError( - "At least one variable/column is set as covariate (``x_cols``) and instrumental " "variable in ``z_cols``." + "At least one variable/column is set as covariate (``x_cols``) and instrumental variable in ``z_cols``." ) self._check_disjoint_sets_t_s() @@ -727,28 +726,25 @@ def _check_disjoint_sets_t_s(self): if self.t_col is not None: t_col_set = {self.t_col} if not t_col_set.isdisjoint(x_cols_set): - raise ValueError(f"{str(self.t_col)} cannot be set as time variable ``t_col`` and covariate in " "``x_cols``.") + raise ValueError(f"{str(self.t_col)} cannot be set as time variable ``t_col`` and covariate in ``x_cols``.") if not t_col_set.isdisjoint(d_cols_set): raise ValueError( - f"{str(self.t_col)} cannot be set as time variable ``t_col`` and treatment variable in " "``d_cols``." + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and treatment variable in ``d_cols``." ) if not t_col_set.isdisjoint(y_col_set): - raise ValueError( - f"{str(self.t_col)} cannot be set as time variable ``t_col`` and outcome variable " "``y_col``." - ) + raise ValueError(f"{str(self.t_col)} cannot be set as time variable ``t_col`` and outcome variable ``y_col``.") if self.z_cols is not None: z_cols_set = set(self.z_cols) if not t_col_set.isdisjoint(z_cols_set): raise ValueError( - f"{str(self.t_col)} cannot be set as time variable ``t_col`` and instrumental " - "variable in ``z_cols``." + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and instrumental variable in ``z_cols``." ) if self.s_col is not None: s_col_set = {self.s_col} if not s_col_set.isdisjoint(x_cols_set): raise ValueError( - f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and covariate in " "``x_cols``." + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and covariate in ``x_cols``." ) if not s_col_set.isdisjoint(d_cols_set): raise ValueError( @@ -757,8 +753,7 @@ def _check_disjoint_sets_t_s(self): ) if not s_col_set.isdisjoint(y_col_set): raise ValueError( - f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and outcome " - "variable ``y_col``." + f"{str(self.s_col)} cannot be set as score or selection variable ``s_col`` and outcome variable ``y_col``." ) if self.z_cols is not None: z_cols_set = set(self.z_cols) @@ -986,9 +981,9 @@ def cluster_cols(self, value): f"{str(value)} of type {str(type(value))} was passed." ) if not len(set(value)) == len(value): - raise ValueError("Invalid cluster variable(s) cluster_cols: " "Contains duplicate values.") + raise ValueError("Invalid cluster variable(s) cluster_cols: Contains duplicate values.") if not set(value).issubset(set(self.all_variables)): - raise ValueError("Invalid cluster variable(s) cluster_cols. " "At least one cluster variable is no data column.") + raise ValueError("Invalid cluster variable(s) cluster_cols. At least one cluster variable is no data column.") self._cluster_cols = value if reset_value: self._check_disjoint_sets() @@ -1064,7 +1059,7 @@ def _check_disjoint_sets_cluster_cols(self): if not y_col_set.isdisjoint(cluster_cols_set): raise ValueError( - f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and cluster " "variable in ``cluster_cols``." + f"{str(self.y_col)} cannot be set as outcome variable ``y_col`` and cluster variable in ``cluster_cols``." ) if not d_cols_set.isdisjoint(cluster_cols_set): raise ValueError( @@ -1074,7 +1069,7 @@ def _check_disjoint_sets_cluster_cols(self): # TODO: Is the following combination allowed, or not? if not x_cols_set.isdisjoint(cluster_cols_set): raise ValueError( - "At least one variable/column is set as covariate (``x_cols``) and cluster " "variable in ``cluster_cols``." + "At least one variable/column is set as covariate (``x_cols``) and cluster variable in ``cluster_cols``." ) if self.z_cols is not None: z_cols_set = set(self.z_cols) @@ -1086,7 +1081,7 @@ def _check_disjoint_sets_cluster_cols(self): if self.t_col is not None: if not t_col_set.isdisjoint(cluster_cols_set): raise ValueError( - f"{str(self.t_col)} cannot be set as time variable ``t_col`` and " "cluster variable in ``cluster_cols``." + f"{str(self.t_col)} cannot be set as time variable ``t_col`` and cluster variable in ``cluster_cols``." ) if self.s_col is not None: if not s_col_set.isdisjoint(cluster_cols_set): diff --git a/doubleml/double_ml_framework.py b/doubleml/double_ml_framework.py index ba350a287..2718629d2 100644 --- a/doubleml/double_ml_framework.py +++ b/doubleml/double_ml_framework.py @@ -245,11 +245,11 @@ def sensitivity_summary(self): if self.sensitivity_params is None: res = header + "Apply sensitivity_analysis() to generate sensitivity_summary." else: - sig_level = f'Significance Level: level={self.sensitivity_params["input"]["level"]}\n' + sig_level = f"Significance Level: level={self.sensitivity_params['input']['level']}\n" scenario_params = ( - f'Sensitivity parameters: cf_y={self.sensitivity_params["input"]["cf_y"]}; ' - f'cf_d={self.sensitivity_params["input"]["cf_d"]}, ' - f'rho={self.sensitivity_params["input"]["rho"]}' + f"Sensitivity parameters: cf_y={self.sensitivity_params['input']['cf_y']}; " + f"cf_d={self.sensitivity_params['input']['cf_d']}, " + f"rho={self.sensitivity_params['input']['rho']}" ) theta_and_ci_col_names = ["CI lower", "theta lower", " theta", "theta upper", "CI upper"] @@ -293,7 +293,6 @@ def sensitivity_summary(self): return res def __add__(self, other): - if isinstance(other, DoubleMLFramework): # internal consistency check self._check_framework_shapes() @@ -354,7 +353,6 @@ def __radd__(self, other): return self.__add__(other) def __sub__(self, other): - if isinstance(other, DoubleMLFramework): # internal consistency check self._check_framework_shapes() @@ -468,7 +466,7 @@ def _calc_sensitivity_analysis(self, cf_y, cf_d, rho, level): _check_in_zero_one(cf_y, "cf_y", include_one=False) _check_in_zero_one(cf_d, "cf_d", include_one=False) if not isinstance(rho, float): - raise TypeError(f"rho must be of float type. " f"{str(rho)} of type {str(type(rho))} was passed.") + raise TypeError(f"rho must be of float type. {str(rho)} of type {str(type(rho))} was passed.") _check_in_zero_one(abs(rho), "The absolute value of rho") _check_in_zero_one(level, "The confidence level", include_zero=False, include_one=False) @@ -505,7 +503,6 @@ def _calc_sensitivity_analysis(self, cf_y, cf_d, rho, level): for i_rep in range(self.n_rep): for i_theta in range(self.n_thetas): - if not self._is_cluster_data: smpls = None cluster_vars = None @@ -678,14 +675,12 @@ def confint(self, joint=False, level=0.95): """ if not isinstance(joint, bool): - raise TypeError("joint must be True or False. " f"Got {str(joint)}.") + raise TypeError(f"joint must be True or False. Got {str(joint)}.") if not isinstance(level, float): - raise TypeError( - "The confidence level must be of float type. " f"{str(level)} of type {str(type(level))} was passed." - ) + raise TypeError(f"The confidence level must be of float type. {str(level)} of type {str(type(level))} was passed.") if (level <= 0) | (level >= 1): - raise ValueError("The confidence level must be in (0,1). " f"{str(level)} was passed.") + raise ValueError(f"The confidence level must be in (0,1). {str(level)} was passed.") # compute critical values alpha = 1 - level @@ -765,9 +760,7 @@ def p_adjust(self, method="romano-wolf"): A numpy array with all corrected p-values for each repetition. """ if not isinstance(method, str): - raise TypeError( - "The p_adjust method must be of str type. " f"{str(method)} of type {str(type(method))} was passed." - ) + raise TypeError(f"The p_adjust method must be of str type. {str(method)} of type {str(type(method))} was passed.") all_p_vals_corrected = np.full_like(self.all_pvals, np.nan) @@ -880,7 +873,7 @@ def sensitivity_plot( """ _check_integer(idx_treatment, "idx_treatment", lower_bound=0, upper_bound=self.n_thetas - 1) if not isinstance(value, str): - raise TypeError("value must be a string. " f"{str(value)} of type {type(value)} was passed.") + raise TypeError(f"value must be a string. {str(value)} of type {type(value)} was passed.") valid_values = ["theta", "ci"] if value not in valid_values: raise ValueError("Invalid value " + value + ". " + "Valid values " + " or ".join(valid_values) + ".") @@ -907,7 +900,6 @@ def sensitivity_plot( contour_values = np.full(shape=(grid_size, grid_size), fill_value=np.nan) for i_cf_d_grid, cf_d_grid in enumerate(cf_d_vec): for i_cf_y_grid, cf_y_grid in enumerate(cf_y_vec): - sens_dict = self._calc_sensitivity_analysis( cf_y=cf_y_grid, cf_d=cf_d_grid, @@ -994,7 +986,7 @@ def _check_and_set_sensitivity_elements(self, doubleml_dict): expected_keys_sensitivity = ["sigma2", "nu2", "psi_sigma2", "psi_nu2", "riesz_rep"] if not all(key in doubleml_dict["sensitivity_elements"].keys() for key in expected_keys_sensitivity): raise ValueError( - "The sensitivity_elements dict must contain the following " "keys: " + ", ".join(expected_keys_sensitivity) + "The sensitivity_elements dict must contain the following keys: " + ", ".join(expected_keys_sensitivity) ) for key in expected_keys_sensitivity: @@ -1042,7 +1034,7 @@ def _check_framework_shapes(self): if self._sensitivity_implemented: if self._sensitivity_elements["sigma2"].shape != (1, self._n_thetas, self.n_rep): raise ValueError( - "The shape of sigma2 does not match the expected shape " f"(1, {self._n_thetas}, {self._n_rep})." + f"The shape of sigma2 does not match the expected shape (1, {self._n_thetas}, {self._n_rep})." ) if self._sensitivity_elements["nu2"].shape != (1, self._n_thetas, self.n_rep): raise ValueError(f"The shape of nu2 does not match the expected shape (1, {self._n_thetas}, {self._n_rep}).") @@ -1073,12 +1065,12 @@ def _check_framework_shapes(self): def _check_treatment_names(self, treatment_names): if not isinstance(treatment_names, list): raise TypeError( - "treatment_names must be a list. " f"Got {str(treatment_names)} of type {str(type(treatment_names))}." + f"treatment_names must be a list. Got {str(treatment_names)} of type {str(type(treatment_names))}." ) is_str = [isinstance(name, str) for name in treatment_names] if not all(is_str): raise TypeError( - "treatment_names must be a list of strings. " f"At least one element is not a string: {str(treatment_names)}." + f"treatment_names must be a list of strings. At least one element is not a string: {str(treatment_names)}." ) if len(treatment_names) != self._n_thetas: raise ValueError( diff --git a/doubleml/double_ml_score_mixins.py b/doubleml/double_ml_score_mixins.py index 4615b8c77..57dd6e623 100644 --- a/doubleml/double_ml_score_mixins.py +++ b/doubleml/double_ml_score_mixins.py @@ -204,7 +204,7 @@ def neg_score(theta): theta_hat = theta_hat_array.item() warnings.warn( "Could not find a root of the score function. " - f"Maximum score value found is {-1*neg_score_val} " + f"Maximum score value found is {-1 * neg_score_val} " f"for parameter theta equal to {theta_hat}. " "No theta found such that the score function evaluates to a positive value." ) diff --git a/doubleml/irm/apo.py b/doubleml/irm/apo.py index 08ca45112..0a42da9a0 100644 --- a/doubleml/irm/apo.py +++ b/doubleml/irm/apo.py @@ -487,7 +487,7 @@ def gapo(self, groups, **kwargs): Best linear Predictor model for group average potential outcomes. """ if not isinstance(groups, pd.DataFrame): - raise TypeError("Groups must be of DataFrame type. " f"Groups of type {str(type(groups))} was passed.") + raise TypeError(f"Groups must be of DataFrame type. Groups of type {str(type(groups))} was passed.") if not all(groups.dtypes == bool) or all(groups.dtypes == int): if groups.shape[1] == 1: diff --git a/doubleml/irm/apos.py b/doubleml/irm/apos.py index 93269dd7c..9fe5617db 100644 --- a/doubleml/irm/apos.py +++ b/doubleml/irm/apos.py @@ -34,7 +34,6 @@ def __init__( trimming_threshold=1e-2, draw_sample_splitting=True, ): - self._dml_data = obj_dml_data self._is_cluster_data = isinstance(obj_dml_data, DoubleMLClusterData) self._check_data(self._dml_data) @@ -600,7 +599,7 @@ def sensitivity_benchmark(self, benchmarking_set, fit_args=None): raise NotImplementedError(f"Sensitivity analysis not yet implemented for {self.__class__.__name__}.") if not isinstance(benchmarking_set, list): raise TypeError( - "benchmarking_set must be a list. " f"{str(benchmarking_set)} of type {type(benchmarking_set)} was passed." + f"benchmarking_set must be a list. {str(benchmarking_set)} of type {type(benchmarking_set)} was passed." ) if len(benchmarking_set) == 0: raise ValueError("benchmarking_set must not be empty.") @@ -610,7 +609,7 @@ def sensitivity_benchmark(self, benchmarking_set, fit_args=None): f"{str(benchmarking_set)} was passed." ) if fit_args is not None and not isinstance(fit_args, dict): - raise TypeError("fit_args must be a dict. " f"{str(fit_args)} of type {type(fit_args)} was passed.") + raise TypeError(f"fit_args must be a dict. {str(fit_args)} of type {type(fit_args)} was passed.") # refit short form of the model x_list_short = [x for x in x_list_long if x not in benchmarking_set] @@ -756,7 +755,6 @@ def causal_contrast(self, reference_levels): return acc def _fit_model(self, i_level, n_jobs_cv=None, store_predictions=True, store_models=False, external_predictions_dict=None): - model = self.modellist[i_level] if external_predictions_dict is not None: external_predictions = external_predictions_dict[self.treatment_levels[i_level]] diff --git a/doubleml/irm/cvar.py b/doubleml/irm/cvar.py index a39ea666c..6190b0789 100644 --- a/doubleml/irm/cvar.py +++ b/doubleml/irm/cvar.py @@ -382,7 +382,7 @@ def _nuisance_tuning( def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) _check_contains_iv(obj_dml_data) _check_zero_one_treatment(self) diff --git a/doubleml/irm/iivm.py b/doubleml/irm/iivm.py index e023f0076..e9dbf8ff1 100644 --- a/doubleml/irm/iivm.py +++ b/doubleml/irm/iivm.py @@ -189,9 +189,9 @@ def __init__( + "subgroups must be a dictionary with keys always_takers and never_takers." ) if not isinstance(subgroups["always_takers"], bool): - raise TypeError("subgroups['always_takers'] must be True or False. " f'Got {str(subgroups["always_takers"])}.') + raise TypeError(f"subgroups['always_takers'] must be True or False. Got {str(subgroups['always_takers'])}.") if not isinstance(subgroups["never_takers"], bool): - raise TypeError("subgroups['never_takers'] must be True or False. " f'Got {str(subgroups["never_takers"])}.') + raise TypeError(f"subgroups['never_takers'] must be True or False. Got {str(subgroups['never_takers'])}.") self.subgroups = subgroups self._external_predictions_implemented = True @@ -223,7 +223,7 @@ def _initialize_ml_nuisance_params(self): def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) one_treat = obj_dml_data.n_treat == 1 binary_treat = type_of_target(obj_dml_data.d) == "binary" diff --git a/doubleml/irm/irm.py b/doubleml/irm/irm.py index 9c0103b2a..539608726 100644 --- a/doubleml/irm/irm.py +++ b/doubleml/irm/irm.py @@ -239,7 +239,7 @@ def _get_weights(self, m_hat=None): def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) if obj_dml_data.z_cols is not None: raise ValueError( @@ -340,7 +340,6 @@ def _nuisance_est(self, smpls, n_jobs_cv, external_predictions, return_models=Fa return psi_elements, preds def _score_elements(self, y, d, g_hat0, g_hat1, m_hat, smpls): - if self.normalize_ipw: m_hat_adj = _normalize_ipw(m_hat, d) else: @@ -518,7 +517,7 @@ def gate(self, groups, **kwargs): Best linear Predictor model for Group Effects. """ if not isinstance(groups, pd.DataFrame): - raise TypeError("Groups must be of DataFrame type. " f"Groups of type {str(type(groups))} was passed.") + raise TypeError(f"Groups must be of DataFrame type. Groups of type {str(type(groups))} was passed.") if not all(groups.dtypes == bool) or all(groups.dtypes == int): if groups.shape[1] == 1: @@ -570,7 +569,7 @@ def policy_tree(self, features, depth=2, **tree_params): _check_integer(depth, "Depth", 0) if not isinstance(features, pd.DataFrame): - raise TypeError("Covariates must be of DataFrame type. " f"Covariates of type {str(type(features))} was passed.") + raise TypeError(f"Covariates must be of DataFrame type. Covariates of type {str(type(features))} was passed.") orth_signal = self.psi_elements["psi_b"].reshape(-1) diff --git a/doubleml/irm/lpq.py b/doubleml/irm/lpq.py index e9c219935..dd912aec2 100644 --- a/doubleml/irm/lpq.py +++ b/doubleml/irm/lpq.py @@ -121,7 +121,7 @@ def __init__( self._kde = _default_kde else: if not callable(kde): - raise TypeError("kde should be either a callable or None. " "%r was passed." % kde) + raise TypeError("kde should be either a callable or None. %r was passed." % kde) self._kde = kde self._normalize_ipw = normalize_ipw @@ -660,7 +660,7 @@ def _nuisance_tuning( def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) _check_zero_one_treatment(self) one_instr = obj_dml_data.n_instr == 1 diff --git a/doubleml/irm/pq.py b/doubleml/irm/pq.py index 7e7430f14..cef152e44 100644 --- a/doubleml/irm/pq.py +++ b/doubleml/irm/pq.py @@ -128,7 +128,7 @@ def __init__( self._kde = _default_kde else: if not callable(kde): - raise TypeError("kde should be either a callable or None. " "%r was passed." % kde) + raise TypeError("kde should be either a callable or None. %r was passed." % kde) self._kde = kde self._normalize_ipw = normalize_ipw @@ -448,7 +448,7 @@ def _nuisance_tuning( def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) _check_contains_iv(obj_dml_data) _check_zero_one_treatment(self) diff --git a/doubleml/irm/qte.py b/doubleml/irm/qte.py index 616be97c2..786635ba7 100644 --- a/doubleml/irm/qte.py +++ b/doubleml/irm/qte.py @@ -102,7 +102,6 @@ def __init__( trimming_threshold=1e-2, draw_sample_splitting=True, ): - self._dml_data = obj_dml_data self._quantiles = np.asarray(quantiles).reshape((-1,)) self._check_quantile() @@ -112,7 +111,7 @@ def __init__( self._kde = _default_kde else: if not callable(kde): - raise TypeError("kde should be either a callable or None. " "%r was passed." % kde) + raise TypeError("kde should be either a callable or None. %r was passed." % kde) self._kde = kde self._normalize_ipw = normalize_ipw @@ -585,7 +584,6 @@ def p_adjust(self, method="romano-wolf"): return p_val def _fit_quantile(self, i_quant, n_jobs_cv=None, store_predictions=True, store_models=False): - model_0 = self.modellist_0[i_quant] model_1 = self.modellist_1[i_quant] @@ -597,7 +595,7 @@ def _fit_quantile(self, i_quant, n_jobs_cv=None, store_predictions=True, store_m def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) _check_zero_one_treatment(self) return @@ -621,7 +619,6 @@ def _initialize_models(self): "draw_sample_splitting": False, } for i_quant in range(self.n_quantiles): - # initialize models for both potential quantiles if self.score == "PQ": model_0 = DoubleMLPQ(quantile=self._quantiles[i_quant], treatment=0, kde=self.kde, **kwargs) diff --git a/doubleml/irm/ssm.py b/doubleml/irm/ssm.py index 1d0965a9f..c63d2076f 100644 --- a/doubleml/irm/ssm.py +++ b/doubleml/irm/ssm.py @@ -184,7 +184,7 @@ def _initialize_ml_nuisance_params(self): def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) if obj_dml_data.z_cols is not None and self._score == "missing-at-random": warnings.warn( diff --git a/doubleml/irm/tests/_utils_apo_manual.py b/doubleml/irm/tests/_utils_apo_manual.py index 5852d0a91..3b74051f3 100644 --- a/doubleml/irm/tests/_utils_apo_manual.py +++ b/doubleml/irm/tests/_utils_apo_manual.py @@ -222,7 +222,6 @@ def fit_sensitivity_elements_apo(y, d, treatment_level, all_coef, predictions, s psi_nu2 = np.full(shape=(n_obs, n_rep, n_treat), fill_value=np.nan) for i_rep in range(n_rep): - m_hat = predictions["ml_m"][:, i_rep, 0] g_hat0 = predictions["ml_g0"][:, i_rep, 0] g_hat1 = predictions["ml_g1"][:, i_rep, 0] diff --git a/doubleml/irm/tests/_utils_irm_manual.py b/doubleml/irm/tests/_utils_irm_manual.py index 774ee6558..27f1a8390 100644 --- a/doubleml/irm/tests/_utils_irm_manual.py +++ b/doubleml/irm/tests/_utils_irm_manual.py @@ -151,7 +151,6 @@ def irm_dml2(y, x, d, g_hat0_list, g_hat1_list, m_hat_list, p_hat_list, smpls, s def var_irm(theta, g_hat0, g_hat1, m_hat, p_hat, u_hat0, u_hat1, d, score, n_obs): - if score == "ATE": var = ( 1 @@ -187,7 +186,6 @@ def var_irm(theta, g_hat0, g_hat1, m_hat, p_hat, u_hat0, u_hat1, d, score, n_obs def irm_orth(g_hat0, g_hat1, m_hat, p_hat, u_hat0, u_hat1, d, score): - if score == "ATE": res = np.mean( g_hat1 - g_hat0 + np.divide(np.multiply(d, u_hat1), m_hat) - np.divide(np.multiply(1.0 - d, u_hat0), 1.0 - m_hat) @@ -322,7 +320,6 @@ def fit_sensitivity_elements_irm(y, d, all_coef, predictions, score, n_rep): psi_nu2 = np.full(shape=(n_obs, n_rep, n_treat), fill_value=np.nan) for i_rep in range(n_rep): - m_hat = predictions["ml_m"][:, i_rep, 0] g_hat0 = predictions["ml_g0"][:, i_rep, 0] g_hat1 = predictions["ml_g1"][:, i_rep, 0] diff --git a/doubleml/irm/tests/_utils_lpq_manual.py b/doubleml/irm/tests/_utils_lpq_manual.py index 527f0b395..e24b5e92f 100644 --- a/doubleml/irm/tests/_utils_lpq_manual.py +++ b/doubleml/irm/tests/_utils_lpq_manual.py @@ -244,7 +244,6 @@ def lpq_dml2(y, d, z, m_z, g_du_z0, g_du_z1, comp_prob, treatment, quantile, ipw def lpq_est(m_z, g_du_z0, g_du_z1, comp_prob, d, y, z, treatment, quantile, ipw_est, coef_bounds): - def compute_score(coef): sign = 2 * treatment - 1.0 score1 = g_du_z1 - g_du_z0 diff --git a/doubleml/irm/tests/_utils_qte_manual.py b/doubleml/irm/tests/_utils_qte_manual.py index aed4fdae6..25de79cd9 100644 --- a/doubleml/irm/tests/_utils_qte_manual.py +++ b/doubleml/irm/tests/_utils_qte_manual.py @@ -22,7 +22,6 @@ def fit_qte( normalize_ipw=True, draw_sample_splitting=True, ): - n_obs = len(y) n_quantiles = len(quantiles) n_folds = len(all_smpls[0]) diff --git a/doubleml/irm/tests/_utils_ssm_manual.py b/doubleml/irm/tests/_utils_ssm_manual.py index d0aa33ed8..1ce4a97bc 100644 --- a/doubleml/irm/tests/_utils_ssm_manual.py +++ b/doubleml/irm/tests/_utils_ssm_manual.py @@ -113,7 +113,6 @@ def fit_nuisance_selection( pi_params=None, m_params=None, ): - ml_g_d1 = clone(learner_g) ml_g_d0 = clone(learner_g) ml_pi = clone(learner_pi) diff --git a/doubleml/irm/tests/test_apo_external_predictions.py b/doubleml/irm/tests/test_apo_external_predictions.py index b2cc5d6c8..aa9b43f37 100644 --- a/doubleml/irm/tests/test_apo_external_predictions.py +++ b/doubleml/irm/tests/test_apo_external_predictions.py @@ -29,7 +29,6 @@ def set_ml_g_ext(request): @pytest.fixture(scope="module") def doubleml_apo_ext_fixture(n_rep, set_ml_m_ext, set_ml_g_ext): - score = "APO" treatment_level = 0 ext_predictions = {"d": {}} diff --git a/doubleml/irm/tests/test_ssm_exceptions.py b/doubleml/irm/tests/test_ssm_exceptions.py index f3eebd19d..d2de330ba 100644 --- a/doubleml/irm/tests/test_ssm_exceptions.py +++ b/doubleml/irm/tests/test_ssm_exceptions.py @@ -85,7 +85,7 @@ def test_ssm_exception_resampling(): msg = "The number of folds must be of int type. 1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, n_folds=1.5) - msg = "The number of repetitions for the sample splitting must be of int type. " "1.5 of type was passed." + msg = "The number of repetitions for the sample splitting must be of int type. 1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLSSM(dml_data_mar, ml_g, ml_pi, ml_m, n_rep=1.5) msg = "The number of folds must be positive. 0 was passed." diff --git a/doubleml/plm/pliv.py b/doubleml/plm/pliv.py index 95e590339..dc0fbd293 100644 --- a/doubleml/plm/pliv.py +++ b/doubleml/plm/pliv.py @@ -200,13 +200,13 @@ def _check_score(self, score): raise ValueError("Invalid score " + score + ". " + "Valid score " + " or ".join(valid_score) + ".") else: if not callable(score): - raise TypeError("score should be either a string or a callable. " "%r was passed." % score) + raise TypeError("score should be either a string or a callable. %r was passed." % score) return score def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) if obj_dml_data.n_instr == 0: raise ValueError( @@ -405,9 +405,7 @@ def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, g_hat, smpls): else: assert callable(self.score) if self._dml_data.n_instr > 1: - raise NotImplementedError( - "Callable score not implemented for DoubleMLPLIV.partialX " "with several instruments." - ) + raise NotImplementedError("Callable score not implemented for DoubleMLPLIV.partialX with several instruments.") else: assert self._dml_data.n_instr == 1 psi_a, psi_b = self.score(y=y, z=z, d=d, l_hat=l_hat, m_hat=m_hat, r_hat=r_hat, g_hat=g_hat, smpls=smpls) diff --git a/doubleml/plm/plr.py b/doubleml/plm/plr.py index 23c65b06b..79ce6c5a7 100644 --- a/doubleml/plm/plr.py +++ b/doubleml/plm/plr.py @@ -114,7 +114,7 @@ def __init__( ) ) elif isinstance(self.score, str) & (self.score == "IV-type"): - warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. " "Set ml_g = clone(ml_l).")) + warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. Set ml_g = clone(ml_l).")) self._learner["ml_g"] = clone(ml_l) self._predict_method = {"ml_l": "predict"} @@ -141,7 +141,7 @@ def _initialize_ml_nuisance_params(self): def _check_data(self, obj_dml_data): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) if obj_dml_data.z_cols is not None: raise ValueError( @@ -418,7 +418,7 @@ def gate(self, groups, **kwargs): """ if not isinstance(groups, pd.DataFrame): - raise TypeError("Groups must be of DataFrame type. " f"Groups of type {str(type(groups))} was passed.") + raise TypeError(f"Groups must be of DataFrame type. Groups of type {str(type(groups))} was passed.") if not all(groups.dtypes == bool) or all(groups.dtypes == int): if groups.shape[1] == 1: groups = pd.get_dummies(groups, prefix="Group", prefix_sep="_") diff --git a/doubleml/rdd/rdd.py b/doubleml/rdd/rdd.py index a8d80eb5a..858ae5ed1 100644 --- a/doubleml/rdd/rdd.py +++ b/doubleml/rdd/rdd.py @@ -108,9 +108,8 @@ def __init__( fs_kernel="triangular", **kwargs, ): - if rdrobust is None: - msg = "rdrobust is not installed. " "Please install it using 'pip install DoubleML[rdd]'" + msg = "rdrobust is not installed. Please install it using 'pip install DoubleML[rdd]'" raise ImportError(msg) self._check_data(obj_dml_data, cutoff) @@ -135,7 +134,7 @@ def __init__( self._h_fs = rdrobust.rdbwselect(y=obj_dml_data.y, x=self._score, fuzzy=fuzzy).bws.values.flatten().max() else: if not isinstance(h_fs, (float)): - raise TypeError("Initial bandwidth 'h_fs' has to be a float. " f"Object of type {str(type(h_fs))} passed.") + raise TypeError(f"Initial bandwidth 'h_fs' has to be a float. Object of type {str(type(h_fs))} passed.") self._h_fs = h_fs self._fs_specification = self._check_fs_specification(fs_specification) @@ -382,11 +381,9 @@ def confint(self, level=0.95): A data frame with the confidence interval(s). """ if not isinstance(level, float): - raise TypeError( - "The confidence level must be of float type. " f"{str(level)} of type {str(type(level))} was passed." - ) + raise TypeError(f"The confidence level must be of float type. {str(level)} of type {str(type(level))} was passed.") if (level <= 0) | (level >= 1): - raise ValueError("The confidence level must be in (0,1). " f"{str(level)} was passed.") + raise ValueError(f"The confidence level must be in (0,1). {str(level)} was passed.") # compute critical values alpha = 1 - level @@ -406,7 +403,6 @@ def confint(self, level=0.95): return df_ci def _fit_nuisance_model(self, outcome, estimator_name, weights, smpls): - # Include transformation of score and cutoff if necessary if self._fs_specification == "cutoff": Z = self._intendend_treatment # instrument for treatment @@ -488,7 +484,7 @@ def _initialize_arrays(self): def _check_data(self, obj_dml_data, cutoff): if not isinstance(obj_dml_data, DoubleMLData): raise TypeError( - "The data must be of DoubleMLData type. " f"{str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." + f"The data must be of DoubleMLData type. {str(obj_dml_data)} of type {str(type(obj_dml_data))} was passed." ) # score checks @@ -499,7 +495,7 @@ def _check_data(self, obj_dml_data, cutoff): raise ValueError("Incompatible data. " + "Score variable has to be continuous. ") if not isinstance(cutoff, (int, float)): - raise TypeError("Cutoff value has to be a float or int. " f"Object of type {str(type(cutoff))} passed.") + raise TypeError(f"Cutoff value has to be a float or int. Object of type {str(type(cutoff))} passed.") if not (obj_dml_data.s.min() <= cutoff <= obj_dml_data.s.max()): raise ValueError("Cutoff value is not within the range of the score variable. ") @@ -560,8 +556,7 @@ def _check_and_set_learner(self, ml_g, ml_m): def _check_and_set_kernel(self, fs_kernel): if not isinstance(fs_kernel, (str, Callable)): raise TypeError( - "fs_kernel must be either a string or a callable. " - f"{str(fs_kernel)} of type {str(type(fs_kernel))} was passed." + f"fs_kernel must be either a string or a callable. {str(fs_kernel)} of type {str(type(fs_kernel))} was passed." ) kernel_functions = { @@ -588,13 +583,12 @@ def _check_and_set_kernel(self, fs_kernel): def _check_fs_specification(self, fs_specification): if not isinstance(fs_specification, str): raise TypeError( - "fs_specification must be a string. " - f"{str(fs_specification)} of type {str(type(fs_specification))} was passed." + f"fs_specification must be a string. {str(fs_specification)} of type {str(type(fs_specification))} was passed." ) expected_specifications = ["cutoff", "cutoff and score", "interacted cutoff and score"] if fs_specification not in expected_specifications: raise ValueError( - f"Invalid fs_specification '{fs_specification}'. " f"Valid specifications are {expected_specifications}." + f"Invalid fs_specification '{fs_specification}'. Valid specifications are {expected_specifications}." ) return fs_specification diff --git a/doubleml/rdd/tests/conftest.py b/doubleml/rdd/tests/conftest.py index 40cfdbb2f..b279ea93c 100644 --- a/doubleml/rdd/tests/conftest.py +++ b/doubleml/rdd/tests/conftest.py @@ -32,7 +32,7 @@ def _predict_dummy(data: DoubleMLData, cutoff, alpha, n_rep, p, fs_specification ci_manual = dml_rdflex.confint(level=1 - alpha) if rdrobust is None: - msg = "rdrobust is not installed. " "Please install it using 'pip install DoubleML[rdd]'" + msg = "rdrobust is not installed. Please install it using 'pip install DoubleML[rdd]'" raise ImportError(msg) rdrobust_model = rdrobust.rdrobust(y=data.y, x=data.s, c=cutoff, level=100 * (1 - alpha), p=p) diff --git a/doubleml/rdd/tests/test_rdd_exceptions.py b/doubleml/rdd/tests/test_rdd_exceptions.py index 7d3c84266..6abf901eb 100644 --- a/doubleml/rdd/tests/test_rdd_exceptions.py +++ b/doubleml/rdd/tests/test_rdd_exceptions.py @@ -135,7 +135,6 @@ def test_rdd_warning_treatment_assignment(): @pytest.mark.ci_rdd @pytest.mark.filterwarnings("ignore:Learner provided for ml_m is probably invalid.*no classifier.*:UserWarning") def test_rdd_exception_learner(): - # ml_g msg = ( r"The ml_g learner LogisticRegression\(\) was identified as classifier but the outcome variable is not" diff --git a/doubleml/tests/_utils_doubleml_sensitivity_manual.py b/doubleml/tests/_utils_doubleml_sensitivity_manual.py index f44bc2486..e8969a5e2 100644 --- a/doubleml/tests/_utils_doubleml_sensitivity_manual.py +++ b/doubleml/tests/_utils_doubleml_sensitivity_manual.py @@ -8,7 +8,6 @@ def doubleml_sensitivity_manual(sensitivity_elements, all_coefs, psi, psi_deriv, cf_y, cf_d, rho, level): - # specify the parameters sigma2 = sensitivity_elements["sigma2"] nu2 = sensitivity_elements["nu2"] diff --git a/doubleml/tests/test_dml_data.py b/doubleml/tests/test_dml_data.py index fbb92f33c..ef5371d28 100644 --- a/doubleml/tests/test_dml_data.py +++ b/doubleml/tests/test_dml_data.py @@ -89,8 +89,8 @@ def test_obj_vs_from_arrays(): dml_data = make_plr_CCDDHNR2018(n_obs=100) df = dml_data.data.copy().iloc[:, :10] - df.columns = [f"X{i+1}" for i in np.arange(7)] + ["y", "d1", "d2"] - dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i+1}" for i in np.arange(7)]) + df.columns = [f"X{i + 1}" for i in np.arange(7)] + ["y", "d1", "d2"] + dml_data = DoubleMLData(df, "y", ["d1", "d2"], [f"X{i + 1}" for i in np.arange(7)]) dml_data_from_array = DoubleMLData.from_arrays( dml_data.data[dml_data.x_cols], dml_data.data[dml_data.y_col], dml_data.data[dml_data.d_cols] ) diff --git a/doubleml/tests/test_exceptions.py b/doubleml/tests/test_exceptions.py index d8cc05a0f..26c80bb3e 100644 --- a/doubleml/tests/test_exceptions.py +++ b/doubleml/tests/test_exceptions.py @@ -489,7 +489,6 @@ def test_doubleml_exception_trimming_rule(): @pytest.mark.ci def test_doubleml_exception_weights(): - msg = "weights must be a numpy array or dictionary. weights of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression(), weights=1) @@ -690,10 +689,7 @@ def test_doubleml_exception_subgroups(): LogisticRegression(), subgroups={"always_takers": True, "never_takers": False, "abs": 5}, ) - msg = ( - "Invalid subgroups {'always_takers': True}. " - "subgroups must be a dictionary with keys always_takers and never_takers." - ) + msg = "Invalid subgroups {'always_takers': True}. subgroups must be a dictionary with keys always_takers and never_takers." with pytest.raises(ValueError, match=msg): _ = DoubleMLIIVM(dml_data_iivm, Lasso(), LogisticRegression(), LogisticRegression(), subgroups={"always_takers": True}) msg = r"subgroups\['always_takers'\] must be True or False. Got 5." @@ -721,7 +717,7 @@ def test_doubleml_exception_resampling(): msg = "The number of folds must be of int type. 1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=1.5) - msg = "The number of repetitions for the sample splitting must be of int type. " "1.5 of type was passed." + msg = "The number of repetitions for the sample splitting must be of int type. 1.5 of type was passed." with pytest.raises(TypeError, match=msg): _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_rep=1.5) msg = "The number of folds must be positive. 0 was passed." @@ -901,9 +897,7 @@ def test_doubleml_exception_tune(): param_grids = {"ml_l": {"alpha": [0.05, 0.5]}, "ml_m": {"alpha": [0.05, 0.5]}} msg = ( - "Invalid scoring_methods neg_mean_absolute_error. " - "scoring_methods must be a dictionary. " - "Valid keys are ml_l and ml_m." + "Invalid scoring_methods neg_mean_absolute_error. scoring_methods must be a dictionary. Valid keys are ml_l and ml_m." ) with pytest.raises(ValueError, match=msg): dml_plr.tune(param_grids, scoring_methods="neg_mean_absolute_error") @@ -1226,7 +1220,7 @@ def test_doubleml_sensitivity_inputs(): _ = dml_irm.sensitivity_plot(idx_treatment=1) # test setter - msg = "_sensitivity_element_est must return sensitivity elements in a dict. " "Got type ." + msg = "_sensitivity_element_est must return sensitivity elements in a dict. Got type ." with pytest.raises(TypeError, match=msg): _ = dml_irm._set_sensitivity_elements(sensitivity_elements=1, i_rep=0, i_treat=0) @@ -1588,7 +1582,7 @@ def test_double_ml_external_predictions(): dml_irm_obj.fit(external_predictions=predictions) predictions = {"d": "test"} - msg = "external_predictions must be a nested dictionary. " "For treatment d a value of type was passed." + msg = "external_predictions must be a nested dictionary. For treatment d a value of type was passed." with pytest.raises(TypeError, match=msg): dml_irm_obj.fit(external_predictions=predictions) diff --git a/doubleml/tests/test_nonlinear_score_mixin.py b/doubleml/tests/test_nonlinear_score_mixin.py index eec0e3c08..d4e9a6950 100644 --- a/doubleml/tests/test_nonlinear_score_mixin.py +++ b/doubleml/tests/test_nonlinear_score_mixin.py @@ -76,7 +76,7 @@ def _check_score(self, score): raise ValueError("Invalid score " + score + ". " + "Valid score " + " or ".join(valid_score) + ".") else: if not callable(score): - raise TypeError("score should be either a string or a callable. " "%r was passed." % score) + raise TypeError("score should be either a string or a callable. %r was passed." % score) return def _check_data(self, obj_dml_data): diff --git a/doubleml/tests/test_return_types.py b/doubleml/tests/test_return_types.py index 7330e8ae8..c4a725fc0 100644 --- a/doubleml/tests/test_return_types.py +++ b/doubleml/tests/test_return_types.py @@ -457,7 +457,6 @@ def _test_sensitivity_return_types(dml_obj, n_rep, n_treat, benchmarking_set): @pytest.mark.ci def test_sensitivity(): - # PLR _test_sensitivity_return_types(plr_obj, n_rep, n_treat, benchmarking_set=["X1"]) diff --git a/doubleml/tests/test_sensitivity.py b/doubleml/tests/test_sensitivity.py index 4f70c6945..70001dfb2 100644 --- a/doubleml/tests/test_sensitivity.py +++ b/doubleml/tests/test_sensitivity.py @@ -43,7 +43,6 @@ def level(request): @pytest.fixture(scope="module") def dml_sensitivity_multitreat_fixture(generate_data_bivariate, n_rep, cf_y, cf_d, rho, level): - # collect data data = generate_data_bivariate x_cols = data.columns[data.columns.str.startswith("X")].tolist() diff --git a/doubleml/tests/test_set_sample_splitting.py b/doubleml/tests/test_set_sample_splitting.py index f9b9d5b5b..97313a00e 100644 --- a/doubleml/tests/test_set_sample_splitting.py +++ b/doubleml/tests/test_set_sample_splitting.py @@ -30,7 +30,6 @@ def _assert_smpls_equal(smpls0, smpls1): @pytest.mark.ci def test_doubleml_set_sample_splitting_tuple(): - # no sample splitting smpls = (np.arange(n_obs), np.arange(n_obs)) dml_plr.set_sample_splitting(smpls) @@ -99,9 +98,7 @@ def test_doubleml_set_sample_splitting_all_list(): [([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8, 9], [0, 1, 2, 3, 4])], (([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]), ([1, 3, 5, 7, 9], [0, 2, 4, 6, 8])), ] - msg = ( - "Invalid partition provided. " "all_smpls is a list where neither all elements are tuples nor all elements are lists." - ) + msg = "Invalid partition provided. all_smpls is a list where neither all elements are tuples nor all elements are lists." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) @@ -131,13 +128,13 @@ def test_doubleml_set_sample_splitting_all_list(): # sample splitting with cross-fitting and two folds that do not form a partition smpls = [[([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]), ([5, 6, 7, 8], [0, 1, 2, 3, 4, 9])]] - msg = "Invalid partition provided. " "At least one inner list does not form a partition." + msg = "Invalid partition provided. At least one inner list does not form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) # repeated no-cross-fitting (does not form a partition) smpls = [[([0, 1, 5, 7, 9], [2, 3, 4, 6, 8])], [([2, 4, 7, 8, 9], [0, 1, 3, 5, 6])], [([0, 1, 4, 6, 8], [2, 3, 5, 7, 9])]] - msg = "Invalid partition provided. " "At least one inner list does not form a partition." + msg = "Invalid partition provided. At least one inner list does not form a partition." with pytest.raises(ValueError, match=msg): dml_plr.set_sample_splitting(smpls) diff --git a/doubleml/utils/_checks.py b/doubleml/utils/_checks.py index 65c9492f6..90833dedb 100644 --- a/doubleml/utils/_checks.py +++ b/doubleml/utils/_checks.py @@ -7,48 +7,48 @@ def _check_in_zero_one(value, name, include_zero=True, include_one=True): if not isinstance(value, float): - raise TypeError(f"{name} must be of float type. " f"{str(value)} of type {str(type(value))} was passed.") + raise TypeError(f"{name} must be of float type. {str(value)} of type {str(type(value))} was passed.") if include_zero & include_one: if (value < 0) | (value > 1): - raise ValueError(f"{name} must be in [0,1]. " f"{str(value)} was passed.") + raise ValueError(f"{name} must be in [0,1]. {str(value)} was passed.") elif (not include_zero) & include_one: if (value <= 0) | (value > 1): - raise ValueError(f"{name} must be in (0,1]. " f"{str(value)} was passed.") + raise ValueError(f"{name} must be in (0,1]. {str(value)} was passed.") elif include_zero & (not include_one): if (value < 0) | (value >= 1): - raise ValueError(f"{name} must be in [0,1). " f"{str(value)} was passed.") + raise ValueError(f"{name} must be in [0,1). {str(value)} was passed.") else: if (value <= 0) | (value >= 1): - raise ValueError(f"{name} must be in (0,1). " f"{str(value)} was passed.") + raise ValueError(f"{name} must be in (0,1). {str(value)} was passed.") return def _check_integer(value, name, lower_bound=None, upper_bound=None): if not isinstance(value, int): - raise TypeError(f"{name} must be an integer." f" {str(value)} of type {str(type(value))} was passed.") + raise TypeError(f"{name} must be an integer. {str(value)} of type {str(type(value))} was passed.") if lower_bound is not None: if value < lower_bound: - raise ValueError(f"{name} must be larger or equal to {lower_bound}. " f"{str(value)} was passed.") + raise ValueError(f"{name} must be larger or equal to {lower_bound}. {str(value)} was passed.") if upper_bound is not None: if value > upper_bound: - raise ValueError(f"{name} must be smaller or equal to {upper_bound}. " f"{str(value)} was passed.") + raise ValueError(f"{name} must be smaller or equal to {upper_bound}. {str(value)} was passed.") return def _check_float(value, name, lower_bound=None, upper_bound=None): if not isinstance(value, float): - raise TypeError(f"{name} must be of float type." f" {str(value)} of type {str(type(value))} was passed.") + raise TypeError(f"{name} must be of float type. {str(value)} of type {str(type(value))} was passed.") if lower_bound is not None: if value < lower_bound: - raise ValueError(f"{name} must be larger or equal to {lower_bound}. " f"{str(value)} was passed.") + raise ValueError(f"{name} must be larger or equal to {lower_bound}. {str(value)} was passed.") if upper_bound is not None: if value > upper_bound: - raise ValueError(f"{name} must be smaller or equal to {upper_bound}. " f"{str(value)} was passed.") + raise ValueError(f"{name} must be smaller or equal to {upper_bound}. {str(value)} was passed.") def _check_bool(value, name): if not isinstance(value, bool): - raise TypeError(f"{name} has to be boolean." f" {str(value)} of type {str(type(value))} was passed.") + raise TypeError(f"{name} has to be boolean. {str(value)} of type {str(type(value))} was passed.") def _check_is_partition(smpls, n_obs): @@ -122,9 +122,9 @@ def _check_score(score, valid_score, allow_callable=True): else: if allow_callable: if not callable(score): - raise TypeError("score should be either a string or a callable. " f"{str(score)} was passed.") + raise TypeError(f"score should be either a string or a callable. {str(score)} was passed.") else: - raise TypeError("score should be a string. " f"{str(score)} was passed.") + raise TypeError(f"score should be a string. {str(score)} was passed.") return @@ -192,7 +192,7 @@ def _check_is_propensity(preds, learner, learner_name, smpls, eps=1e-12): test_indices = np.concatenate([test_index for _, test_index in smpls]) if any((preds[test_indices] < eps) | (preds[test_indices] > 1 - eps)): warnings.warn( - f"Propensity predictions from learner {str(learner)} for" f" {learner_name} are close to zero or one (eps={eps})." + f"Propensity predictions from learner {str(learner)} for {learner_name} are close to zero or one (eps={eps})." ) return @@ -213,41 +213,35 @@ def _check_benchmarks(benchmarks): if benchmarks is not None: if not isinstance(benchmarks, dict): raise TypeError( - "benchmarks has to be either None or a dictionary. " - f"{str(benchmarks)} of type {type(benchmarks)} was passed." + f"benchmarks has to be either None or a dictionary. {str(benchmarks)} of type {type(benchmarks)} was passed." ) if not set(benchmarks.keys()) == {"cf_y", "cf_d", "name"}: - raise ValueError( - "benchmarks has to be a dictionary with keys cf_y, cf_d and name. " f"Got {str(benchmarks.keys())}." - ) + raise ValueError(f"benchmarks has to be a dictionary with keys cf_y, cf_d and name. Got {str(benchmarks.keys())}.") value_lengths = [len(value) for value in benchmarks.values()] if not len(set(value_lengths)) == 1: - raise ValueError("benchmarks has to be a dictionary with values of same length. " f"Got {str(value_lengths)}.") + raise ValueError(f"benchmarks has to be a dictionary with values of same length. Got {str(value_lengths)}.") for i in range(value_lengths[0]): for key in ["cf_y", "cf_d"]: _check_in_zero_one(benchmarks[key][i], f"benchmarks {key}", include_zero=True, include_one=False) if not isinstance(benchmarks["name"][i], str): raise TypeError( "benchmarks name must be of string type. " - f'{str(benchmarks["name"][i])} of type {str(type(benchmarks["name"][i]))} was passed.' + f"{str(benchmarks['name'][i])} of type {str(type(benchmarks['name'][i]))} was passed." ) return def _check_weights(weights, score, n_obs, n_rep): if weights is not None: - # check general type if (not isinstance(weights, np.ndarray)) and (not isinstance(weights, dict)): - raise TypeError( - "weights must be a numpy array or dictionary. " f"weights of type {str(type(weights))} was passed." - ) + raise TypeError(f"weights must be a numpy array or dictionary. weights of type {str(type(weights))} was passed.") # check shape if isinstance(weights, np.ndarray): if (weights.ndim != 1) or weights.shape[0] != n_obs: - raise ValueError(f"weights must have shape ({n_obs},). " f"weights of shape {weights.shape} was passed.") + raise ValueError(f"weights must have shape ({n_obs},). weights of shape {weights.shape} was passed.") if not np.all(0 <= weights): raise ValueError("All weights values must be greater or equal 0.") if weights.sum() == 0: @@ -257,7 +251,7 @@ def _check_weights(weights, score, n_obs, n_rep): if score == "ATTE": if not isinstance(weights, np.ndarray): raise TypeError( - "weights must be a numpy array for ATTE score. " f"weights of type {str(type(weights))} was passed." + f"weights must be a numpy array for ATTE score. weights of type {str(type(weights))} was passed." ) is_binary = np.all((np.power(weights, 2) - weights) == 0) @@ -269,13 +263,12 @@ def _check_weights(weights, score, n_obs, n_rep): assert score == "ATE" expected_keys = ["weights", "weights_bar"] if not set(weights.keys()) == set(expected_keys): - raise ValueError(f"weights must have keys {expected_keys}. " f"keys {str(weights.keys())} were passed.") + raise ValueError(f"weights must have keys {expected_keys}. keys {str(weights.keys())} were passed.") expected_shapes = [(n_obs,), (n_obs, n_rep)] if weights["weights"].shape != expected_shapes[0]: raise ValueError( - f"weights must have shape {expected_shapes[0]}. " - f"weights of shape {weights['weights'].shape} was passed." + f"weights must have shape {expected_shapes[0]}. weights of shape {weights['weights'].shape} was passed." ) if weights["weights_bar"].shape != expected_shapes[1]: raise ValueError( @@ -349,9 +342,8 @@ def _check_external_predictions(external_predictions, valid_treatments, valid_le def _check_bootstrap(method, n_rep_boot): - if (not isinstance(method, str)) | (method not in ["Bayes", "normal", "wild"]): - raise ValueError('Method must be "Bayes", "normal" or "wild". ' f"Got {str(method)}.") + raise ValueError(f'Method must be "Bayes", "normal" or "wild". Got {str(method)}.') if not isinstance(n_rep_boot, int): raise TypeError( @@ -359,7 +351,7 @@ def _check_bootstrap(method, n_rep_boot): f"{str(n_rep_boot)} of type {str(type(n_rep_boot))} was passed." ) if n_rep_boot < 1: - raise ValueError("The number of bootstrap replications must be positive. " f"{str(n_rep_boot)} was passed.") + raise ValueError(f"The number of bootstrap replications must be positive. {str(n_rep_boot)} was passed.") return @@ -397,9 +389,9 @@ def _check_set(x): def _check_resampling_specification(n_folds, n_rep): if not isinstance(n_folds, int): - raise TypeError("The number of folds must be of int type. " f"{str(n_folds)} of type {str(type(n_folds))} was passed.") + raise TypeError(f"The number of folds must be of int type. {str(n_folds)} of type {str(type(n_folds))} was passed.") if n_folds < 2: - raise ValueError("The number of folds greater or equal to 2. " f"{str(n_folds)} was passed.") + raise ValueError(f"The number of folds greater or equal to 2. {str(n_folds)} was passed.") if not isinstance(n_rep, int): raise TypeError( @@ -407,7 +399,7 @@ def _check_resampling_specification(n_folds, n_rep): f"{str(n_rep)} of type {str(type(n_rep))} was passed." ) if n_rep < 1: - raise ValueError("The number of repetitions for the sample splitting has to be positive. " f"{str(n_rep)} was passed.") + raise ValueError(f"The number of repetitions for the sample splitting has to be positive. {str(n_rep)} was passed.") return @@ -427,33 +419,30 @@ def _check_cluster_sample_splitting(all_smpls_cluster, dml_data, n_rep, n_folds) n_rep_cluster = len(all_smpls_cluster) if n_rep_cluster != n_rep: raise ValueError( - "Invalid samples provided. " "Number of repetitions for all_smpls and all_smpls_cluster must be the same." + "Invalid samples provided. Number of repetitions for all_smpls and all_smpls_cluster must be the same." ) for i_rep in range(n_rep): n_folds_cluster = len(all_smpls_cluster[i_rep]) if n_folds_cluster != n_folds: - raise ValueError( - "Invalid samples provided. " "Number of folds for all_smpls and all_smpls_cluster must be the same." - ) + raise ValueError("Invalid samples provided. Number of folds for all_smpls and all_smpls_cluster must be the same.") for i_cluster in range(dml_data.n_cluster_vars): this_cluster_var = dml_data.cluster_vars[:, i_cluster] clusters = np.unique(this_cluster_var) cluster_partition = [all_smpls_cluster[0][0][0][i_cluster], all_smpls_cluster[0][0][1][i_cluster]] is_cluster_partition = _check_cluster_partitions(cluster_partition, clusters) if not is_cluster_partition: - raise ValueError("Invalid cluster partition provided. " "At least one inner list does not form a partition.") + raise ValueError("Invalid cluster partition provided. At least one inner list does not form a partition.") smpls_cluster = all_smpls_cluster return smpls_cluster def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_data): - if isinstance(all_smpls, tuple): if not len(all_smpls) == 2: raise ValueError( - "Invalid partition provided. " "Tuple for train_ind and test_ind must consist of exactly two elements." + "Invalid partition provided. Tuple for train_ind and test_ind must consist of exactly two elements." ) all_smpls = _check_smpl_split_tpl(all_smpls, dml_data.n_obs) if _check_is_partition([all_smpls], dml_data.n_obs) & _check_is_partition( @@ -463,18 +452,17 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d n_folds = 1 smpls = [[all_smpls]] else: - raise ValueError("Invalid partition provided. " "Tuple provided that doesn't form a partition.") + raise ValueError("Invalid partition provided. Tuple provided that doesn't form a partition.") else: if not isinstance(all_smpls, list): raise TypeError( - "all_smpls must be of list or tuple type. " f"{str(all_smpls)} of type {str(type(all_smpls))} was passed." + f"all_smpls must be of list or tuple type. {str(all_smpls)} of type {str(type(all_smpls))} was passed." ) all_tuple = all([isinstance(tpl, tuple) for tpl in all_smpls]) if all_tuple: if not all([len(tpl) == 2 for tpl in all_smpls]): raise ValueError( - "Invalid partition provided. " - "All tuples for train_ind and test_ind must consist of exactly two elements." + "Invalid partition provided. All tuples for train_ind and test_ind must consist of exactly two elements." ) n_rep = 1 all_smpls = _check_smpl_split(all_smpls, dml_data.n_obs) @@ -486,7 +474,7 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d n_folds = len(all_smpls) smpls = _check_all_smpls([all_smpls], dml_data.n_obs, check_intersect=True) else: - raise ValueError("Invalid partition provided. " "Tuples provided that don't form a partition.") + raise ValueError("Invalid partition provided. Tuples provided that don't form a partition.") else: all_list = all([isinstance(smpl, list) for smpl in all_smpls]) if not all_list: @@ -501,12 +489,11 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d all_pairs = all([all([len(tpl) == 2 for tpl in smpl]) for smpl in all_smpls]) if not all_pairs: raise ValueError( - "Invalid partition provided. " - "All tuples for train_ind and test_ind must consist of exactly two elements." + "Invalid partition provided. All tuples for train_ind and test_ind must consist of exactly two elements." ) n_folds_each_smpl = np.array([len(smpl) for smpl in all_smpls]) if not np.all(n_folds_each_smpl == n_folds_each_smpl[0]): - raise ValueError("Invalid partition provided. " "Different number of folds for repeated sample splitting.") + raise ValueError("Invalid partition provided. Different number of folds for repeated sample splitting.") all_smpls = _check_all_smpls(all_smpls, dml_data.n_obs) smpls_are_partitions = [_check_is_partition(smpl, dml_data.n_obs) for smpl in all_smpls] @@ -515,7 +502,7 @@ def _check_sample_splitting(all_smpls, all_smpls_cluster, dml_data, is_cluster_d n_folds = int(n_folds_each_smpl[0]) smpls = _check_all_smpls(all_smpls, dml_data.n_obs, check_intersect=True) else: - raise ValueError("Invalid partition provided. " "At least one inner list does not form a partition.") + raise ValueError("Invalid partition provided. At least one inner list does not form a partition.") if is_cluster_data: smpls_cluster = _check_cluster_sample_splitting(all_smpls_cluster, dml_data, n_rep, n_folds) diff --git a/doubleml/utils/_estimation.py b/doubleml/utils/_estimation.py index c33317777..408c2f516 100644 --- a/doubleml/utils/_estimation.py +++ b/doubleml/utils/_estimation.py @@ -274,7 +274,6 @@ def _aggregate_coefs_and_ses(all_coefs, all_ses, var_scaling_factors): def _var_est(psi, psi_deriv, smpls, is_cluster_data, cluster_vars=None, smpls_cluster=None, n_folds_per_cluster=None): - if not is_cluster_data: # psi and psi_deriv should be of shape (n_obs, ...) var_scaling_factor = psi.shape[0] diff --git a/doubleml/utils/_plots.py b/doubleml/utils/_plots.py index b5785272c..67b449b38 100644 --- a/doubleml/utils/_plots.py +++ b/doubleml/utils/_plots.py @@ -14,7 +14,6 @@ def _sensitivity_contour_plot( benchmarks=None, fill=True, ): - if fill: text_col = "white" contours_coloring = "heatmap" diff --git a/doubleml/utils/blp.py b/doubleml/utils/blp.py index a671147a7..13844ee77 100644 --- a/doubleml/utils/blp.py +++ b/doubleml/utils/blp.py @@ -27,20 +27,19 @@ class DoubleMLBLP: """ def __init__(self, orth_signal, basis, is_gate=False): - if not isinstance(orth_signal, np.ndarray): - raise TypeError("The signal must be of np.ndarray type. " f"Signal of type {str(type(orth_signal))} was passed.") + raise TypeError(f"The signal must be of np.ndarray type. Signal of type {str(type(orth_signal))} was passed.") if orth_signal.ndim != 1: raise ValueError( - "The signal must be of one dimensional. " f"Signal of dimensions {str(orth_signal.ndim)} was passed." + f"The signal must be of one dimensional. Signal of dimensions {str(orth_signal.ndim)} was passed." ) if not isinstance(basis, pd.DataFrame): - raise TypeError("The basis must be of DataFrame type. " f"Basis of type {str(type(basis))} was passed.") + raise TypeError(f"The basis must be of DataFrame type. Basis of type {str(type(basis))} was passed.") if not basis.columns.is_unique: - raise ValueError("Invalid pd.DataFrame: " "Contains duplicate column names.") + raise ValueError("Invalid pd.DataFrame: Contains duplicate column names.") self._orth_signal = orth_signal self._basis = basis @@ -159,14 +158,12 @@ def confint(self, basis=None, joint=False, level=0.95, n_rep_boot=500): A data frame with the confidence interval(s). """ if not isinstance(joint, bool): - raise TypeError("joint must be True or False. " f"Got {str(joint)}.") + raise TypeError(f"joint must be True or False. Got {str(joint)}.") if not isinstance(level, float): - raise TypeError( - "The confidence level must be of float type. " f"{str(level)} of type {str(type(level))} was passed." - ) + raise TypeError(f"The confidence level must be of float type. {str(level)} of type {str(type(level))} was passed.") if (level <= 0) | (level >= 1): - raise ValueError("The confidence level must be in (0,1). " f"{str(level)} was passed.") + raise ValueError(f"The confidence level must be in (0,1). {str(level)} was passed.") if not isinstance(n_rep_boot, int): raise TypeError( @@ -174,7 +171,7 @@ def confint(self, basis=None, joint=False, level=0.95, n_rep_boot=500): f"{str(n_rep_boot)} of type {str(type(n_rep_boot))} was passed." ) if n_rep_boot < 1: - raise ValueError("The number of bootstrap replications must be positive. " f"{str(n_rep_boot)} was passed.") + raise ValueError(f"The number of bootstrap replications must be positive. {str(n_rep_boot)} was passed.") if self._blp_model is None: raise ValueError("Apply fit() before confint().") diff --git a/doubleml/utils/gain_statistics.py b/doubleml/utils/gain_statistics.py index 79ff8406c..482d45cc1 100644 --- a/doubleml/utils/gain_statistics.py +++ b/doubleml/utils/gain_statistics.py @@ -32,7 +32,7 @@ def gain_statistics(dml_long, dml_short): expected_keys = ["sigma2", "nu2"] if not all(key in sensitivity_elements_long.keys() for key in expected_keys): raise ValueError( - "dml_long does not contain the necessary sensitivity elements. " "Required keys are: " + str(expected_keys) + "dml_long does not contain the necessary sensitivity elements. Required keys are: " + str(expected_keys) ) if not isinstance(sensitivity_elements_short, dict): raise TypeError( @@ -41,17 +41,17 @@ def gain_statistics(dml_long, dml_short): ) if not all(key in sensitivity_elements_short.keys() for key in expected_keys): raise ValueError( - "dml_short does not contain the necessary sensitivity elements. " "Required keys are: " + str(expected_keys) + "dml_short does not contain the necessary sensitivity elements. Required keys are: " + str(expected_keys) ) for key in expected_keys: if not isinstance(sensitivity_elements_long[key], np.ndarray): raise TypeError( - "dml_long does not contain the necessary sensitivity elements. " f"Expected numpy.ndarray for key {key}." + f"dml_long does not contain the necessary sensitivity elements. Expected numpy.ndarray for key {key}." ) if not isinstance(sensitivity_elements_short[key], np.ndarray): raise TypeError( - "dml_short does not contain the necessary sensitivity elements. " f"Expected numpy.ndarray for key {key}." + f"dml_short does not contain the necessary sensitivity elements. Expected numpy.ndarray for key {key}." ) if len(sensitivity_elements_long[key].shape) != 3 or sensitivity_elements_long[key].shape[0] != 1: raise ValueError( diff --git a/doubleml/utils/global_learner.py b/doubleml/utils/global_learner.py index 4acc87357..0949f9562 100644 --- a/doubleml/utils/global_learner.py +++ b/doubleml/utils/global_learner.py @@ -13,7 +13,6 @@ class GlobalRegressor(BaseEstimator, RegressorMixin): """ def __init__(self, base_estimator): - if not is_regressor(base_estimator): raise ValueError(f"base_estimator must be a regressor. Got {base_estimator.__class__.__name__} instead.") @@ -62,7 +61,6 @@ class GlobalClassifier(BaseEstimator, ClassifierMixin): """ def __init__(self, base_estimator): - if not is_classifier(base_estimator): raise ValueError(f"base_estimator must be a classifier. Got {base_estimator.__class__.__name__} instead.") diff --git a/doubleml/utils/policytree.py b/doubleml/utils/policytree.py index 59ae0cf9b..ca771ad62 100644 --- a/doubleml/utils/policytree.py +++ b/doubleml/utils/policytree.py @@ -29,20 +29,19 @@ class DoubleMLPolicyTree: """ def __init__(self, orth_signal, features, depth=2, **tree_params): - if not isinstance(orth_signal, np.ndarray): - raise TypeError("The signal must be of np.ndarray type. " f"Signal of type {str(type(orth_signal))} was passed.") + raise TypeError(f"The signal must be of np.ndarray type. Signal of type {str(type(orth_signal))} was passed.") if orth_signal.ndim != 1: raise ValueError( - "The signal must be of one dimensional. " f"Signal of dimensions {str(orth_signal.ndim)} was passed." + f"The signal must be of one dimensional. Signal of dimensions {str(orth_signal.ndim)} was passed." ) if not isinstance(features, pd.DataFrame): - raise TypeError("The features must be of DataFrame type. " f"Features of type {str(type(features))} was passed.") + raise TypeError(f"The features must be of DataFrame type. Features of type {str(type(features))} was passed.") if not features.columns.is_unique: - raise ValueError("Invalid pd.DataFrame: " "Contains duplicate column names.") + raise ValueError("Invalid pd.DataFrame: Contains duplicate column names.") self._orth_signal = orth_signal self._features = features @@ -145,12 +144,11 @@ def predict(self, features): check_is_fitted(self._policy_tree, msg="Policy Tree not yet fitted. Call fit before predict.") if not isinstance(features, pd.DataFrame): - raise TypeError("The features must be of DataFrame type. " f"Features of type {str(type(features))} was passed.") + raise TypeError(f"The features must be of DataFrame type. Features of type {str(type(features))} was passed.") if not set(features.keys()) == set(self._features.keys()): raise KeyError( - f"The features must have the keys {self._features.keys()}. " - f"Features with keys {features.keys()} were passed." + f"The features must have the keys {self._features.keys()}. Features with keys {features.keys()} were passed." ) predictions = self.policy_tree.predict(features) diff --git a/doubleml/utils/resampling.py b/doubleml/utils/resampling.py index 6345ed9a6..188d2f248 100644 --- a/doubleml/utils/resampling.py +++ b/doubleml/utils/resampling.py @@ -11,7 +11,7 @@ def __init__(self, n_folds, n_rep, n_obs, stratify=None): if n_folds < 2: raise ValueError( - "n_folds must be greater than 1. " "You can use set_sample_splitting with a tuple to only use one fold." + "n_folds must be greater than 1. You can use set_sample_splitting with a tuple to only use one fold." ) if self.stratify is None: @@ -27,7 +27,6 @@ def split_samples(self): class DoubleMLClusterResampling: def __init__(self, n_folds, n_rep, n_obs, n_cluster_vars, cluster_vars): - self.n_folds = n_folds self.n_rep = n_rep self.n_obs = n_obs diff --git a/doubleml/utils/tests/test_global_learners.py b/doubleml/utils/tests/test_global_learners.py index 5c8844dd8..f549f71c6 100644 --- a/doubleml/utils/tests/test_global_learners.py +++ b/doubleml/utils/tests/test_global_learners.py @@ -24,7 +24,6 @@ def classifier(request): @pytest.fixture(scope="module") def gl_fixture(regressor, classifier): - global_reg = GlobalRegressor(base_estimator=regressor) weighted_reg = clone(regressor) unweighted_reg = clone(regressor) diff --git a/doubleml/utils/tests/test_var_est_and_aggregation.py b/doubleml/utils/tests/test_var_est_and_aggregation.py index 22abe6954..969864b4b 100644 --- a/doubleml/utils/tests/test_var_est_and_aggregation.py +++ b/doubleml/utils/tests/test_var_est_and_aggregation.py @@ -26,7 +26,6 @@ def test_var_est_and_aggr_fixture(n_rep, n_coefs): for i_coef in range(n_coefs): n_obs = np.random.randint(100, 200) for i_rep in range(n_rep): - psi = np.random.normal(size=(n_obs)) psi_deriv = np.ones((n_obs)) From 61442824a0fe4b973565d3ef5bf4b9a910bb9762 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 12:05:40 +0100 Subject: [PATCH 21/28] Update pyproject.toml --- pyproject.toml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9d3f0eb86..52cac56c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,6 @@ exclude = ''' | build | dist | doc/_build - | doubleml/externals )/ ''' @@ -67,16 +66,7 @@ exclude = ''' # max line length for black line-length = 127 target-version = "py39" -exclude = [ - ".git", - ".mypy_cache", - ".vscode", - "build", - "dist", - "doc/_build", - "doc/auto_examples", - "doubleml/externals", -] + [tool.ruff.lint] # all rules can be found here: https://beta.ruff.rs/docs/rules/ From 43fba264e20dd5793b9c84abda7da64b399d4b7a Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 12:52:39 +0100 Subject: [PATCH 22/28] update workflow to include black and ruff --- .github/workflows/pytest.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 0fb27f571..9355ca739 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -45,12 +45,16 @@ jobs: python -m pip install --upgrade pip python -m pip install flake8 pip install -e .[dev,rdd] - - name: Lint with flake8 + - name: Lint with ruff run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + ruff check . --select E9,F63,F7,F82 --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + ruff check . --exit-zero --line-length 127 --statistics + - name: Check code formatting with black + run: | + python -m pip install black + black --check . - name: Test with pytest if: | matrix.config.os != 'ubuntu-latest' || From 13500050946c248576abbba83199169792ac1962 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 13:07:40 +0100 Subject: [PATCH 23/28] Update pytest.yml --- .github/workflows/pytest.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9355ca739..f1ce2941c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -43,17 +43,15 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pip install -e .[dev,rdd] - name: Lint with ruff run: | # stop the build if there are Python syntax errors or undefined names - ruff check . --select E9,F63,F7,F82 --statistics + ruff check . --select E9,F63,F7,F82 --output-format=full --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - ruff check . --exit-zero --line-length 127 --statistics + ruff check . --exit-zero --line-length 127 --output-format=full --statistics - name: Check code formatting with black run: | - python -m pip install black black --check . - name: Test with pytest if: | From 308c66c3d998d43693a48e2022e340c596fa4c83 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 13:20:39 +0100 Subject: [PATCH 24/28] Update CONTRIBUTING.md --- CONTRIBUTING.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4151eeaf9..e11a2bb02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,7 +119,13 @@ $ pytest . - [x] Check whether your changes adhere to the **PEP8 standards**. For the check you can use the following code ```bash -$ git diff upstream/main -u -- "*.py" | flake8 --diff --max-line-length=127 +$ git diff upstream/main -u -- "*.py" | ruff check --diff +``` + +- [x] Check wether the code formatting adheres to the **Black code style** +by running +```bash +$ black . --check --diff ``` If your PR is still **work in progress**, please consider marking it a **draft PR** From b7b54e669e2c9a9df0024897d1e9009b42130cbe Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 13:36:22 +0100 Subject: [PATCH 25/28] add pre-commit --- .pre-commit-config.yaml | 17 +++++++++++++++++ pyproject.toml | 1 + 2 files changed, 18 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..076aa2a29 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.1 + hooks: + - id: ruff + args: ["--fix", "--output-format=full"] +- repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 52cac56c5..c68c69066 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dev = [ "lightgbm", "black>=24.3.0", "ruff>=0.5.1", + "pre-commit>=4.0.1", ] [tool.black] From 7c6c5cdc3e700051855714c6a7237e342d109a43 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 13:50:10 +0100 Subject: [PATCH 26/28] remove trailing whitespaces --- CONTRIBUTING.md | 6 +++--- README.md | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e11a2bb02..d5ca44026 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,7 +81,7 @@ $ git merge upstream/main 5. **Install DoubleML in editable mode** (more details can be found [here](https://docs.doubleml.org/stable/intro/install.html#python-building-the-package-from-source)) -via +via ```bash $ pip install --editable .[dev, rdd] ``` @@ -171,7 +171,7 @@ The source code for the website, user guide, example gallery, etc. is available ### Contribute to the API Documentation The **API documentation** is generated from **docstrings** in the source code. -It can be generated locally (dev requirements sphinx and pydata-sphinx-theme need to be installed) via +It can be generated locally (dev requirements sphinx and pydata-sphinx-theme need to be installed) via ```bash $ cd doc/ $ make html @@ -181,7 +181,7 @@ $ make html The **documentation of DoubleML** is hosted at [https://docs.doubleml.org](https://docs.doubleml.org). The **source code** for the website, user guide, example gallery, etc. is available in a **separate repository [doubleml-docs](https://github.com/DoubleML/doubleml-docs)**. -Changes, issues and PRs for the documentation (except the API documentation) should be discussed in the +Changes, issues and PRs for the documentation (except the API documentation) should be discussed in the [doubleml-docs](https://github.com/DoubleML/doubleml-docs) repo. We welcome contributions to the user guide, especially case studies for the [example gallery](https://docs.doubleml.org/stable/examples/index.html). diff --git a/README.md b/README.md index e713d494d..37a1894e8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The Python package **DoubleML** provides an implementation of the double / debia It is built on top of [scikit-learn](https://scikit-learn.org) (Pedregosa et al., 2011). Note that the Python package was developed together with an R twin based on [mlr3](https://mlr3.mlr-org.com/). -The R package is also available on [GitHub](https://github.com/DoubleML/doubleml-for-r) and +The R package is also available on [GitHub](https://github.com/DoubleML/doubleml-for-r) and [![CRAN Version](https://www.r-pkg.org/badges/version/DoubleML)](https://cran.r-project.org/package=DoubleML). ## Documentation and Maintenance @@ -27,7 +27,7 @@ Bugs can be reported to the issue tracker at ## Main Features -Double / debiased machine learning [(Chernozhukov et al. (2018))](https://doi.org/10.1111/ectj.12097) for +Double / debiased machine learning [(Chernozhukov et al. (2018))](https://doi.org/10.1111/ectj.12097) for - Partially linear regression models (PLR) - Partially linear IV regression models (PLIV) @@ -46,14 +46,14 @@ This object-oriented implementation allows a high flexibility for the model spec - ... the resampling schemes, - ... the double machine learning algorithm, - ... the Neyman orthogonal score functions, -- ... +- ... It further can be readily extended with regards to - ... new model classes that come with Neyman orthogonal score functions being linear in the target parameter, - ... alternative score functions via callables, - ... alternative resampling schemes, -- ... +- ... ![An overview of the OOP structure of the DoubleML package is given in the graphic available at https://github.com/DoubleML/doubleml-for-py/blob/main/doc/oop.svg](https://raw.githubusercontent.com/DoubleML/doubleml-for-py/main/doc/oop.svg) @@ -106,7 +106,7 @@ Bibtex-entry: ``` @article{DoubleML2022, - title = {{DoubleML} -- {A}n Object-Oriented Implementation of Double Machine Learning in {P}ython}, + title = {{DoubleML} -- {A}n Object-Oriented Implementation of Double Machine Learning in {P}ython}, author = {Philipp Bach and Victor Chernozhukov and Malte S. Kurz and Martin Spindler}, journal = {Journal of Machine Learning Research}, year = {2022}, From ccced2fe9988689af01ccc9c3f9426e1cac09071 Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 13:51:27 +0100 Subject: [PATCH 27/28] run end-of-file-fixer --- .github/ISSUE_TEMPLATE/api_docu.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .pre-commit-config.yaml | 2 +- CODE_OF_CONDUCT.md | 2 +- LICENSE | 2 +- MANIFEST.in | 2 +- doc/_templates/class.rst | 2 +- doc/oop.svg | 2 +- pyproject.toml | 2 +- pytest.ini | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/api_docu.yml b/.github/ISSUE_TEMPLATE/api_docu.yml index c2004a531..dd23ee0b4 100644 --- a/.github/ISSUE_TEMPLATE/api_docu.yml +++ b/.github/ISSUE_TEMPLATE/api_docu.yml @@ -11,4 +11,4 @@ body: required: true - type: textarea attributes: - label: Suggested alternative or fix \ No newline at end of file + label: Suggested alternative or fix diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f6f09dd41..5cf5c8000 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -76,4 +76,4 @@ body: import sklearn; print("Scikit-Learn", sklearn.__version__) ``` validations: - required: true \ No newline at end of file + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 43088fb56..2ec778f0c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -17,4 +17,4 @@ body: label: Did you consider alternatives to the proposed solution. If yes, please describe - type: textarea attributes: - label: Comments, context or references \ No newline at end of file + label: Comments, context or references diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 076aa2a29..0a391f696 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,4 +14,4 @@ repos: - repo: https://github.com/psf/black rev: 24.10.0 hooks: - - id: black \ No newline at end of file + - id: black diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5cc83c2c6..ff4c7c8c4 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -130,4 +130,4 @@ enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE index f20a88b19..d4d3f03ce 100644 --- a/LICENSE +++ b/LICENSE @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index cb5457a94..f23447ba7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include LICENSE -include pytest.ini \ No newline at end of file +include pytest.ini diff --git a/doc/_templates/class.rst b/doc/_templates/class.rst index b4ec353ca..c7ca556c1 100644 --- a/doc/_templates/class.rst +++ b/doc/_templates/class.rst @@ -34,4 +34,4 @@ .. automethod:: {{ name }}.{{ item }} {% endif %} {%- endfor %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/doc/oop.svg b/doc/oop.svg index c63dc8404..9b2d1ed80 100644 --- a/doc/oop.svg +++ b/doc/oop.svg @@ -2419,4 +2419,4 @@ - \ No newline at end of file + diff --git a/pyproject.toml b/pyproject.toml index c68c69066..a8d359f37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,4 +81,4 @@ ignore = [ [project.urls] Documentation = "https://docs.doubleml.org" Source = "https://github.com/DoubleML/doubleml-for-py" -"Bug Tracker" = "https://github.com/DoubleML/doubleml-for-py/issues" \ No newline at end of file +"Bug Tracker" = "https://github.com/DoubleML/doubleml-for-py/issues" diff --git a/pytest.ini b/pytest.ini index 696d3343a..3582830cb 100644 --- a/pytest.ini +++ b/pytest.ini @@ -14,4 +14,4 @@ filterwarnings = ignore:.*Propensity score is close to 0 or 1. Trimming is at 0.01 and 0.99 is applied.*:UserWarning ignore:.*Sensitivity analysis not implemented for callable scores.*:UserWarning ignore:.*Subsample has not common support. Results are based on adjusted propensities.*:UserWarning - ignore:.*Treatment probability within bandwidth left from cutoff higher than right from cutoff.\nTreatment assignment might be based on the wrong side of the cutoff.*:UserWarning \ No newline at end of file + ignore:.*Treatment probability within bandwidth left from cutoff higher than right from cutoff.\nTreatment assignment might be based on the wrong side of the cutoff.*:UserWarning From 72c32d5788654c29ea5e69efff476d585a1f814c Mon Sep 17 00:00:00 2001 From: SvenKlaassen Date: Mon, 13 Jan 2025 14:55:16 +0100 Subject: [PATCH 28/28] add pre-commit hooks to contributing --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5ca44026..bd6a465d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,6 +131,22 @@ $ black . --check --diff If your PR is still **work in progress**, please consider marking it a **draft PR** (see also [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)). +### (Optional) Set up pre-commit Hooks + +To ensure code quality and consistency before committing your changes, we recommend using [pre-commit hooks](https://pre-commit.com/). Pre-commit hooks will automatically run checks like code formatting and linting on your staged files. + +1. **Install hooks**: + If you haven't already, install the required hooks by running: + ```bash + $ pre-commit install + ``` + +2. **Run pre-commit manually**: + To run the pre-commit checks manually, use: + ```bash + $ pre-commit run --all-files + ``` + ### Unit Tests and Test Coverage We use the package **pytest for unit testing**. Unit testing is considered to be a fundamental part of the development workflow.