From c86b332a34ad86bb9d27d46f43efcce2fe217e8f Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 10 Feb 2022 16:35:48 +0100 Subject: [PATCH 01/47] adapt the nuisance estimation for the IV type score in the PLR model --- doubleml/double_ml_plr.py | 73 +++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index c5d4432c..2905d0ac 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -116,7 +116,8 @@ def __init__(self, self._initialize_ml_nuisance_params() 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_l', 'ml_g', 'ml_m']} def _check_score(self, score): if isinstance(score, str): @@ -144,10 +145,10 @@ def _nuisance_est(self, smpls, n_jobs_cv): x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) - # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls, n_jobs=n_jobs_cv, + # nuisance l + l_hat = _dml_cv_predict(self._learner['ml_g'], x, y, 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, self._learner['ml_g'], 'ml_g', smpls) + _check_finite_predictions(l_hat, self._learner['ml_g'], 'ml_g', smpls) # nuisance m m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, @@ -163,28 +164,41 @@ def _nuisance_est(self, smpls, n_jobs_cv): 'observed to be binary with values 0 and 1. Make sure that for classifiers ' 'probabilities and not labels are predicted.') - psi_a, psi_b = self._score_elements(y, d, g_hat, m_hat, smpls) + # an estimate of g is obtained for the IV-type score and callable scores + g_hat = None + if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): + # get an initial estimate for theta using the partialling out score + psi_a = -np.multiply(d - m_hat, d - m_hat) + psi_b = np.multiply(d - m_hat, y - l_hat) + theta_initial = -np.mean(psi_b) / np.mean(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_l'), method=self._predict_method['ml_g']) + _check_finite_predictions(g_hat, self._learner['ml_g'], 'ml_g', smpls) + + psi_a, psi_b = self._score_elements(y, d, l_hat, g_hat, m_hat, smpls) preds = {'ml_g': g_hat, + 'ml_l': l_hat, 'ml_m': m_hat} return psi_a, psi_b, preds - def _score_elements(self, y, d, g_hat, m_hat, smpls): + def _score_elements(self, y, d, l_hat, g_hat, m_hat, smpls): # compute residuals - u_hat = y - g_hat + u_hat = y - l_hat v_hat = d - m_hat - v_hatd = np.multiply(v_hat, d) if isinstance(self.score, str): if self.score == 'IV-type': - psi_a = -v_hatd + psi_a = - np.multiply(v_hat, d) + psi_b = np.multiply(v_hat, y - g_hat) else: assert self.score == 'partialling out' psi_a = -np.multiply(v_hat, v_hat) - psi_b = np.multiply(v_hat, u_hat) + psi_b = np.multiply(v_hat, u_hat) else: assert callable(self.score) - psi_a, psi_b = self.score(y, d, g_hat, m_hat, smpls) + psi_a, psi_b = self.score(y, d, l_hat, g_hat, m_hat, smpls) return psi_a, psi_b @@ -200,21 +214,44 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ 'ml_m': None} train_inds = [train_index for (train_index, _) in smpls] - g_tune_res = _dml_tune(y, x, train_inds, + l_tune_res = _dml_tune(y, 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) 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] + l_best_params = [xx.best_params_ for xx in l_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} + # an ML model for g is obtained for the IV-type score and callable scores + if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): + # 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) + for idx, (train_index, _) in enumerate(smpls): + l_hat[train_index] = l_tune_res[idx].predict(x[train_index, :]) + m_hat[train_index] = m_tune_res[idx].predict(x[train_index, :]) + psi_a = -np.multiply(d - m_hat, d - m_hat) + psi_b = np.multiply(d - m_hat, y - l_hat) + theta_initial = -np.mean(psi_b) / np.mean(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_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} + else: + assert self.score == 'partialling out' + params = {'ml_l': l_best_params, + 'ml_m': m_best_params} + tune_res = {'g_tune': l_tune_res, + 'm_tune': m_tune_res} res = {'params': params, 'tune_res': tune_res} From 174ebadcf1ee65928f1450831e48fb76b580f4be Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Wed, 9 Mar 2022 10:44:06 +0100 Subject: [PATCH 02/47] some fixes for the adaption of nuisance learning for the IV-type score implemented in c86b332a34ad86bb9d27d46f43efcce2fe217e8f --- doubleml/double_ml_plr.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 2905d0ac..d4046f35 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -116,8 +116,12 @@ def __init__(self, self._initialize_ml_nuisance_params() 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_l', 'ml_g', 'ml_m']} + if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} + for learner in ['ml_l', 'ml_g', 'ml_m']} + else: + self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} + for learner in ['ml_l', 'ml_m']} def _check_score(self, score): if isinstance(score, str): @@ -147,7 +151,7 @@ def _nuisance_est(self, smpls, n_jobs_cv): # nuisance l l_hat = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_g'), method=self._predict_method['ml_g']) + est_params=self._get_params('ml_l'), method=self._predict_method['ml_g']) _check_finite_predictions(l_hat, self._learner['ml_g'], 'ml_g', smpls) # nuisance m @@ -170,10 +174,10 @@ def _nuisance_est(self, smpls, n_jobs_cv): # get an initial estimate for theta using the partialling out score psi_a = -np.multiply(d - m_hat, d - m_hat) psi_b = np.multiply(d - m_hat, y - l_hat) - theta_initial = -np.mean(psi_b) / np.mean(psi_a) + 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_l'), method=self._predict_method['ml_g']) + est_params=self._get_params('ml_g'), method=self._predict_method['ml_g']) _check_finite_predictions(g_hat, self._learner['ml_g'], 'ml_g', smpls) psi_a, psi_b = self._score_elements(y, d, l_hat, g_hat, m_hat, smpls) @@ -234,7 +238,7 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ m_hat[train_index] = m_tune_res[idx].predict(x[train_index, :]) psi_a = -np.multiply(d - m_hat, d - m_hat) psi_b = np.multiply(d - m_hat, y - l_hat) - theta_initial = -np.mean(psi_b) / np.mean(psi_a) + 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) @@ -250,7 +254,7 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ assert self.score == 'partialling out' params = {'ml_l': l_best_params, 'ml_m': m_best_params} - tune_res = {'g_tune': l_tune_res, + tune_res = {'l_tune': l_tune_res, 'm_tune': m_tune_res} res = {'params': params, From dd9ae4c1fac2bc4bd0134c047b8a6cb080b04e92 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Wed, 9 Mar 2022 10:45:20 +0100 Subject: [PATCH 03/47] align unit tests with the adapted implementation of nuisance learning for IV-type score c86b332a34ad86bb9d27d46f43efcce2fe217e8f --- doubleml/tests/_utils_plr_manual.py | 139 ++++++++++++------ doubleml/tests/test_doubleml_exceptions.py | 15 +- doubleml/tests/test_doubleml_scores.py | 3 +- doubleml/tests/test_plr.py | 28 +++- doubleml/tests/test_plr_classifier.py | 2 +- doubleml/tests/test_plr_multi_treat.py | 2 +- doubleml/tests/test_plr_no_cross_fit.py | 32 ++-- doubleml/tests/test_plr_rep_cross.py | 2 +- .../tests/test_plr_set_ml_nuisance_pars.py | 5 +- doubleml/tests/test_plr_tune.py | 18 ++- 10 files changed, 167 insertions(+), 79 deletions(-) diff --git a/doubleml/tests/_utils_plr_manual.py b/doubleml/tests/_utils_plr_manual.py index 4f1ba7cf..08b15539 100644 --- a/doubleml/tests/_utils_plr_manual.py +++ b/doubleml/tests/_utils_plr_manual.py @@ -7,7 +7,7 @@ def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, m_params=None, + n_rep=1, g_params=None, l_params=None, m_params=None, use_other_treat_as_covariate=True): n_obs = len(y) n_d = d.shape[1] @@ -15,12 +15,14 @@ def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, thetas = list() ses = list() all_g_hat = list() + all_l_hat = list() all_m_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] thetas_this_rep = np.full(n_d, np.nan) ses_this_rep = np.full(n_d, np.nan) all_g_hat_this_rep = list() + all_l_hat_this_rep = list() all_m_hat_this_rep = list() for i_d in range(n_d): @@ -29,14 +31,16 @@ def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, else: xd = x - g_hat, m_hat, thetas_this_rep[i_d], ses_this_rep[i_d] = fit_plr_single_split( - y, xd, d[:, i_d], learner_g, learner_m, smpls, dml_procedure, score, g_params, m_params) + g_hat, l_hat, m_hat, thetas_this_rep[i_d], ses_this_rep[i_d] = fit_plr_single_split( + y, xd, d[:, i_d], learner_g, learner_m, smpls, dml_procedure, score, g_params, l_params, m_params) all_g_hat_this_rep.append(g_hat) + all_l_hat_this_rep.append(l_hat) all_m_hat_this_rep.append(m_hat) thetas.append(thetas_this_rep) ses.append(ses_this_rep) all_g_hat.append(all_g_hat_this_rep) + all_l_hat.append(all_l_hat_this_rep) all_m_hat.append(all_m_hat_this_rep) theta = np.full(n_d, np.nan) @@ -49,24 +53,26 @@ def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, res = {'theta': theta, 'se': se, 'thetas': thetas, 'ses': ses, - 'all_g_hat': all_g_hat, 'all_m_hat': all_m_hat} + 'all_g_hat': all_g_hat, 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat} return res def fit_plr(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, m_params=None): + n_rep=1, g_params=None, l_params=None, m_params=None): n_obs = len(y) thetas = np.zeros(n_rep) ses = np.zeros(n_rep) all_g_hat = list() + all_l_hat = list() all_m_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat, thetas[i_rep], ses[i_rep] = fit_plr_single_split( - y, x, d, learner_g, learner_m, smpls, dml_procedure, score, g_params, m_params) + g_hat, l_hat, m_hat, thetas[i_rep], ses[i_rep] = fit_plr_single_split( + y, x, d, learner_g, learner_m, smpls, dml_procedure, score, g_params, l_params, m_params) all_g_hat.append(g_hat) + all_l_hat.append(l_hat) all_m_hat.append(m_hat) theta = np.median(thetas) @@ -74,78 +80,124 @@ def fit_plr(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, score, res = {'theta': theta, 'se': se, 'thetas': thetas, 'ses': ses, - 'all_g_hat': all_g_hat, 'all_m_hat': all_m_hat} + 'all_g_hat': all_g_hat, 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat} return res -def fit_plr_single_split(y, x, d, learner_g, learner_m, smpls, dml_procedure, score, g_params=None, m_params=None): +def fit_plr_single_split(y, x, d, learner_g, learner_m, smpls, dml_procedure, score, + g_params=None, l_params=None, m_params=None): + fit_g = (score == 'IV-type') | callable(score) if is_classifier(learner_m): - g_hat, m_hat = fit_nuisance_plr_classifier(y, x, d, - learner_g, learner_m, smpls, - g_params, m_params) + g_hat, l_hat, m_hat = fit_nuisance_plr_classifier(y, x, d, + learner_g, learner_m, smpls, fit_g, + g_params, l_params, m_params) else: - g_hat, m_hat = fit_nuisance_plr(y, x, d, - learner_g, learner_m, smpls, - g_params, m_params) + g_hat, l_hat, m_hat = fit_nuisance_plr(y, x, d, + learner_g, learner_m, smpls, fit_g, + g_params, l_params, m_params) if dml_procedure == 'dml1': theta, se = plr_dml1(y, x, d, - g_hat, m_hat, + g_hat, l_hat, m_hat, smpls, score) else: assert dml_procedure == 'dml2' theta, se = plr_dml2(y, x, d, - g_hat, m_hat, + g_hat, l_hat, m_hat, smpls, score) - return g_hat, m_hat, theta, se + return g_hat, l_hat, m_hat, theta, se -def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, g_params=None, m_params=None): - ml_g = clone(learner_g) - g_hat = fit_predict(y, x, ml_g, g_params, smpls) +def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, fit_g=True, + g_params=None, l_params=None, m_params=None): + ml_l = clone(learner_g) + l_hat = fit_predict(y, x, ml_l, l_params, smpls) ml_m = clone(learner_m) m_hat = fit_predict(d, x, ml_m, m_params, smpls) - return g_hat, m_hat + if fit_g: + u_hat, v_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls, 'partialling out') + psi_a = -np.multiply(v_hat, v_hat) + psi_b = np.multiply(v_hat, u_hat) + 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) + else: + g_hat = None + + return g_hat, l_hat, m_hat -def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, g_params=None, m_params=None): - ml_g = clone(learner_g) - g_hat = fit_predict(y, x, ml_g, g_params, smpls) +def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, fit_g=True, + g_params=None, l_params=None, m_params=None): + ml_l = clone(learner_g) + l_hat = fit_predict(y, x, ml_l, l_params, smpls) ml_m = clone(learner_m) m_hat = fit_predict_proba(d, x, ml_m, m_params, smpls) - return g_hat, m_hat + if fit_g: + u_hat, v_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls, 'partialling out') + psi_a = -np.multiply(v_hat, v_hat) + psi_b = np.multiply(v_hat, u_hat) + 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) + else: + g_hat = None + + return g_hat, l_hat, m_hat -def tune_nuisance_plr(y, x, d, ml_g, ml_m, smpls, n_folds_tune, param_grid_g, param_grid_m): - g_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune) +def tune_nuisance_plr(y, x, d, ml_g, ml_m, smpls, n_folds_tune, param_grid_g, param_grid_m, tune_g=True): + l_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune) 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] + if tune_g: + l_hat = np.full_like(y, np.nan) + m_hat = np.full_like(d, np.nan) + for idx, (train_index, _) in enumerate(smpls): + l_hat[train_index] = l_tune_res[idx].predict(x[train_index, :]) + m_hat[train_index] = m_tune_res[idx].predict(x[train_index, :]) + 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 = 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 = [] + + l_best_params = [xx.best_params_ for xx in l_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - return g_best_params, m_best_params + return g_best_params, l_best_params, m_best_params -def compute_plr_residuals(y, d, g_hat, m_hat, smpls): +def compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score): + # Note that u_hat is not the same for score = 'partialling out' vs. score = 'IV-type' u_hat = np.full_like(y, np.nan, dtype='float64') v_hat = np.full_like(d, np.nan, dtype='float64') for idx, (_, test_index) in enumerate(smpls): - u_hat[test_index] = y[test_index] - g_hat[idx] + if score == 'partialling out': + u_hat[test_index] = y[test_index] - l_hat[idx] + else: + assert score == 'IV-type' + u_hat[test_index] = y[test_index] - g_hat[idx] v_hat[test_index] = d[test_index] - m_hat[idx] return u_hat, v_hat -def plr_dml1(y, x, d, g_hat, m_hat, smpls, score): +def plr_dml1(y, x, d, g_hat, l_hat, m_hat, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - u_hat, v_hat = compute_plr_residuals(y, d, g_hat, m_hat, smpls) + u_hat, v_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score) for idx, (_, test_index) in enumerate(smpls): thetas[idx] = plr_orth(v_hat[test_index], u_hat[test_index], d[test_index], score) @@ -157,14 +209,15 @@ def plr_dml1(y, x, d, g_hat, m_hat, smpls, score): assert len(smpls) == 1 test_index = smpls[0][1] n_obs = len(test_index) - se = np.sqrt(var_plr(theta_hat, d[test_index], u_hat[test_index], v_hat[test_index], score, n_obs)) + se = np.sqrt(var_plr(theta_hat, d[test_index], u_hat[test_index], v_hat[test_index], + score, n_obs)) return theta_hat, se -def plr_dml2(y, x, d, g_hat, m_hat, smpls, score): +def plr_dml2(y, x, d, g_hat, l_hat, m_hat, smpls, score): n_obs = len(y) - u_hat, v_hat = compute_plr_residuals(y, d, g_hat, m_hat, smpls) + u_hat, v_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score) theta_hat = plr_orth(v_hat, u_hat, d, score) se = np.sqrt(var_plr(theta_hat, d, u_hat, v_hat, score, n_obs)) @@ -193,7 +246,7 @@ def plr_orth(v_hat, u_hat, d, score): return res -def boot_plr(y, d, thetas, ses, all_g_hat, all_m_hat, +def boot_plr(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=1, apply_cross_fitting=True): all_boot_theta = list() @@ -208,7 +261,7 @@ def boot_plr(y, d, thetas, ses, all_g_hat, all_m_hat, weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_theta, boot_t_stat = boot_plr_single_split( - thetas[i_rep], y, d, all_g_hat[i_rep], all_m_hat[i_rep], smpls, + thetas[i_rep], y, d, all_g_hat[i_rep], all_l_hat[i_rep], all_m_hat[i_rep], smpls, score, ses[i_rep], weights, n_rep_boot, apply_cross_fitting) all_boot_theta.append(boot_theta) @@ -220,7 +273,7 @@ def boot_plr(y, d, thetas, ses, all_g_hat, all_m_hat, return boot_theta, boot_t_stat -def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_m_hat, +def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=1, apply_cross_fitting=True): n_d = d.shape[1] @@ -240,7 +293,7 @@ def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_m_hat, for i_d in range(n_d): boot_theta[i_d, :], boot_t_stat[i_d, :] = boot_plr_single_split( thetas[i_rep][i_d], y, d[:, i_d], - all_g_hat[i_rep][i_d], all_m_hat[i_rep][i_d], + all_g_hat[i_rep][i_d], all_l_hat[i_rep][i_d], all_m_hat[i_rep][i_d], smpls, score, ses[i_rep][i_d], weights, n_rep_boot, apply_cross_fitting) all_boot_theta.append(boot_theta) @@ -252,9 +305,9 @@ def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_m_hat, return boot_theta, boot_t_stat -def boot_plr_single_split(theta, y, d, g_hat, m_hat, +def boot_plr_single_split(theta, y, d, g_hat, l_hat, m_hat, smpls, score, se, weights, n_rep, apply_cross_fitting): - u_hat, v_hat = compute_plr_residuals(y, d, g_hat, m_hat, smpls) + u_hat, v_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score) if apply_cross_fitting: if score == 'partialling out': diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index f281fc6a..13c554f4 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -15,6 +15,7 @@ ml_m = Lasso() ml_r = Lasso() dml_plr = DoubleMLPLR(dml_data, ml_g, ml_m) +dml_plr_iv_type = DoubleMLPLR(dml_data, ml_g, ml_m, score='IV-type') dml_data_irm = make_irm_data(n_obs=10) dml_data_iivm = make_iivm_data(n_obs=10) @@ -218,9 +219,17 @@ def test_doubleml_exception_no_cross_fit(): @pytest.mark.ci def test_doubleml_exception_get_params(): - msg = 'Invalid nuisance learner ml_r. Valid nuisance learner ml_g 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.' + 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_g or ml_m.' + with pytest.raises(ValueError, match=msg): + dml_plr_iv_type.get_params('ml_r') + + @pytest.mark.ci @@ -375,12 +384,12 @@ def test_doubleml_exception_tune(): @pytest.mark.ci def test_doubleml_exception_set_ml_nuisance_params(): - msg = 'Invalid nuisance learner g. Valid nuisance learner ml_g 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.' with pytest.raises(ValueError, match=msg): - dml_plr.set_ml_nuisance_params('ml_g', 'y', {'alpha': 0.1}) + dml_plr.set_ml_nuisance_params('ml_l', 'y', {'alpha': 0.1}) class _DummyNoSetParams: diff --git a/doubleml/tests/test_doubleml_scores.py b/doubleml/tests/test_doubleml_scores.py index 3a4a26ad..b249e278 100644 --- a/doubleml/tests/test_doubleml_scores.py +++ b/doubleml/tests/test_doubleml_scores.py @@ -63,10 +63,11 @@ def test_plr_callable_vs_str_score(): @pytest.mark.ci def test_plr_callable_vs_pred_export(): preds = dml_plr_callable_score.predictions + l_hat = preds['ml_l'].squeeze() g_hat = preds['ml_g'].squeeze() m_hat = preds['ml_m'].squeeze() psi_a, psi_b = plr_score(dml_data_plr.y, dml_data_plr.d, - g_hat, m_hat, + l_hat, g_hat, m_hat, dml_plr_callable_score.smpls[0]) assert np.allclose(dml_plr.psi_a.squeeze(), psi_a, diff --git a/doubleml/tests/test_plr.py b/doubleml/tests/test_plr.py index 971abf55..4c6b6813 100644 --- a/doubleml/tests/test_plr.py +++ b/doubleml/tests/test_plr.py @@ -77,7 +77,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], all_smpls, score, bootstrap, n_rep_boot) np.random.seed(3141) @@ -157,24 +157,38 @@ def dml_plr_ols_manual_fixture(generate_data1, score, dml_procedure): smpls = dml_plr_obj.smpls[0] - g_hat = [] + l_hat = [] + l_hat_vec = np.full_like(y, np.nan) for (train_index, test_index) in smpls: ols_est = scipy.linalg.lstsq(x[train_index], y[train_index])[0] - g_hat.append(np.dot(x[test_index], ols_est)) + preds = np.dot(x[test_index], ols_est) + l_hat.append(preds) + l_hat_vec[test_index] = preds m_hat = [] + m_hat_vec = np.full_like(d, np.nan) for (train_index, test_index) in smpls: ols_est = scipy.linalg.lstsq(x[train_index], d[train_index])[0] - m_hat.append(np.dot(x[test_index], ols_est)) + preds = np.dot(x[test_index], ols_est) + m_hat.append(preds) + m_hat_vec[test_index] = preds + + g_hat = [] + 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] + g_hat.append(np.dot(x[test_index], ols_est)) if dml_procedure == 'dml1': res_manual, se_manual = plr_dml1(y, x, d, - g_hat, m_hat, + g_hat, l_hat, m_hat, smpls, score) else: assert dml_procedure == 'dml2' res_manual, se_manual = plr_dml2(y, x, d, - g_hat, m_hat, + g_hat, l_hat, m_hat, smpls, score) res_dict = {'coef': dml_plr_obj.coef, @@ -186,7 +200,7 @@ def dml_plr_ols_manual_fixture(generate_data1, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, [res_manual], [se_manual], - [g_hat], [m_hat], + [g_hat], [l_hat], [m_hat], [smpls], score, bootstrap, n_rep_boot) np.random.seed(3141) diff --git a/doubleml/tests/test_plr_classifier.py b/doubleml/tests/test_plr_classifier.py index af2e14b8..d8aa037f 100644 --- a/doubleml/tests/test_plr_classifier.py +++ b/doubleml/tests/test_plr_classifier.py @@ -74,7 +74,7 @@ def dml_plr_binary_classifier_fixture(learner, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], all_smpls, score, bootstrap, n_rep_boot) np.random.seed(3141) diff --git a/doubleml/tests/test_plr_multi_treat.py b/doubleml/tests/test_plr_multi_treat.py index 669fe180..299023c7 100644 --- a/doubleml/tests/test_plr_multi_treat.py +++ b/doubleml/tests/test_plr_multi_treat.py @@ -96,7 +96,7 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, boot_theta, boot_t_stat = boot_plr_multitreat( y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], all_smpls, score, bootstrap, n_rep_boot, n_rep) diff --git a/doubleml/tests/test_plr_no_cross_fit.py b/doubleml/tests/test_plr_no_cross_fit.py index d1e5c94f..393b9a83 100644 --- a/doubleml/tests/test_plr_no_cross_fit.py +++ b/doubleml/tests/test_plr_no_cross_fit.py @@ -79,7 +79,7 @@ def dml_plr_no_cross_fit_fixture(generate_data1, learner, score, n_folds): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], [smpls], score, bootstrap, n_rep_boot, apply_cross_fitting=False) @@ -161,18 +161,20 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): thetas = np.zeros(n_rep) ses = np.zeros(n_rep) all_g_hat = list() + all_l_hat = list() all_m_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat = fit_nuisance_plr(y, x, d, - clone(learner), clone(learner), smpls) + g_hat, l_hat, m_hat = fit_nuisance_plr(y, x, d, + clone(learner), clone(learner), smpls) all_g_hat.append(g_hat) + all_l_hat.append(l_hat) all_m_hat.append(m_hat) thetas[i_rep], ses[i_rep] = plr_dml1(y, x, d, - all_g_hat[i_rep], all_m_hat[i_rep], + all_g_hat[i_rep], all_l_hat[i_rep], all_m_hat[i_rep], smpls, score) res_manual = np.median(thetas) @@ -188,7 +190,7 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, thetas, ses, - all_g_hat, all_m_hat, + all_g_hat, all_l_hat, all_m_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=n_rep, apply_cross_fitting=False) @@ -276,18 +278,22 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo smpls = all_smpls[0] smpls = [smpls[0]] + tune_g = score == 'IV-type' if tune_on_folds: - g_params, m_params = tune_nuisance_plr(y, x, d, - clone(ml_g), clone(ml_m), smpls, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g_params, l_params, m_params = tune_nuisance_plr(y, x, d, + clone(ml_g), clone(ml_m), smpls, n_folds_tune, + par_grid['ml_g'], par_grid['ml_m'], + tune_g) else: xx = [(np.arange(len(y)), np.array([]))] - g_params, m_params = tune_nuisance_plr(y, x, d, - clone(ml_g), clone(ml_m), xx, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g_params, l_params, m_params = tune_nuisance_plr(y, x, d, + clone(ml_g), clone(ml_m), xx, n_folds_tune, + par_grid['ml_g'], par_grid['ml_m'], + tune_g) res_manual = fit_plr(y, x, d, clone(ml_m), clone(ml_g), - [smpls], dml_procedure, score, g_params=g_params, m_params=m_params) + [smpls], dml_procedure, score, + g_params=g_params, l_params=l_params, m_params=m_params) res_dict = {'coef': dml_plr_obj.coef, 'coef_manual': res_manual['theta'], @@ -298,7 +304,7 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], [smpls], score, bootstrap, n_rep_boot, apply_cross_fitting=False) diff --git a/doubleml/tests/test_plr_rep_cross.py b/doubleml/tests/test_plr_rep_cross.py index 8f5eea2c..391ac37b 100644 --- a/doubleml/tests/test_plr_rep_cross.py +++ b/doubleml/tests/test_plr_rep_cross.py @@ -83,7 +83,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure, n_rep): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], all_smpls, score, bootstrap, n_rep_boot, n_rep) np.random.seed(3141) diff --git a/doubleml/tests/test_plr_set_ml_nuisance_pars.py b/doubleml/tests/test_plr_set_ml_nuisance_pars.py index 69d830a1..e61b5abe 100644 --- a/doubleml/tests/test_plr_set_ml_nuisance_pars.py +++ b/doubleml/tests/test_plr_set_ml_nuisance_pars.py @@ -56,8 +56,11 @@ def dml_plr_fixture(generate_data1, score, dml_procedure): n_folds, score=score, dml_procedure=dml_procedure) - dml_plr_obj_ext_set_par.set_ml_nuisance_params('ml_g', 'd', {'alpha': alpha}) + 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, diff --git a/doubleml/tests/test_plr_tune.py b/doubleml/tests/test_plr_tune.py index 4b25dfa0..3967b0dd 100644 --- a/doubleml/tests/test_plr_tune.py +++ b/doubleml/tests/test_plr_tune.py @@ -91,21 +91,23 @@ def dml_plr_fixture(generate_data2, learner_g, learner_m, score, dml_procedure, all_smpls = draw_smpls(n_obs, n_folds) smpls = all_smpls[0] + tune_g = score == 'IV-type' if tune_on_folds: - g_params, m_params = tune_nuisance_plr(y, x, d, - clone(learner_g), clone(learner_m), smpls, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g_params, l_params, m_params = tune_nuisance_plr(y, x, d, + clone(learner_g), clone(learner_m), smpls, n_folds_tune, + par_grid['ml_g'], par_grid['ml_m'], tune_g) else: xx = [(np.arange(len(y)), np.array([]))] - g_params, m_params = tune_nuisance_plr(y, x, d, - clone(learner_g), clone(learner_m), xx, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m']) + g_params, l_params, m_params = tune_nuisance_plr(y, x, d, + clone(learner_g), clone(learner_m), xx, n_folds_tune, + par_grid['ml_g'], par_grid['ml_m'], 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_g), clone(learner_m), all_smpls, dml_procedure, score, - g_params=g_params, m_params=m_params) + g_params=g_params, l_params=l_params, m_params=m_params) res_dict = {'coef': dml_plr_obj.coef, 'coef_manual': res_manual['theta'], @@ -116,7 +118,7 @@ def dml_plr_fixture(generate_data2, learner_g, learner_m, score, dml_procedure, for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], all_smpls, score, bootstrap, n_rep_boot) np.random.seed(3141) From 64866dfcef988ac205755b07347df89f59129f35 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 21 Apr 2022 15:09:51 +0200 Subject: [PATCH 04/47] refactor the functional PLR implementation --- doubleml/tests/_utils_plr_manual.py | 78 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/doubleml/tests/_utils_plr_manual.py b/doubleml/tests/_utils_plr_manual.py index 08b15539..4b963d7a 100644 --- a/doubleml/tests/_utils_plr_manual.py +++ b/doubleml/tests/_utils_plr_manual.py @@ -119,9 +119,9 @@ def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, fit_g=True, m_hat = fit_predict(d, x, ml_m, m_params, smpls) if fit_g: - u_hat, v_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls, 'partialling out') - psi_a = -np.multiply(v_hat, v_hat) - psi_b = np.multiply(v_hat, u_hat) + y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls) + psi_a = -np.multiply(d_minus_m_hat, d_minus_m_hat) + psi_b = np.multiply(d_minus_m_hat, y_minus_l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) ml_g = clone(learner_g) @@ -141,9 +141,9 @@ def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, fit_g=True m_hat = fit_predict_proba(d, x, ml_m, m_params, smpls) if fit_g: - u_hat, v_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls, 'partialling out') - psi_a = -np.multiply(v_hat, v_hat) - psi_b = np.multiply(v_hat, u_hat) + y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls) + psi_a = -np.multiply(d_minus_m_hat, d_minus_m_hat) + psi_b = np.multiply(d_minus_m_hat, y_minus_l_hat) theta_initial = -np.mean(psi_b) / np.mean(psi_a) ml_g = clone(learner_g) @@ -180,36 +180,36 @@ def tune_nuisance_plr(y, x, d, ml_g, ml_m, smpls, n_folds_tune, param_grid_g, pa return g_best_params, l_best_params, m_best_params -def compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score): - # Note that u_hat is not the same for score = 'partialling out' vs. score = 'IV-type' - u_hat = np.full_like(y, np.nan, dtype='float64') - v_hat = np.full_like(d, np.nan, dtype='float64') +def compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls): + y_minus_l_hat = np.full_like(y, np.nan, dtype='float64') + y_minus_g_hat = np.full_like(y, np.nan, dtype='float64') + d_minus_m_hat = np.full_like(d, np.nan, dtype='float64') for idx, (_, test_index) in enumerate(smpls): - if score == 'partialling out': - u_hat[test_index] = y[test_index] - l_hat[idx] - else: - assert score == 'IV-type' - u_hat[test_index] = y[test_index] - g_hat[idx] - v_hat[test_index] = d[test_index] - m_hat[idx] - return u_hat, v_hat + y_minus_l_hat[test_index] = y[test_index] - l_hat[idx] + if g_hat is not None: + y_minus_g_hat[test_index] = y[test_index] - g_hat[idx] + d_minus_m_hat[test_index] = d[test_index] - m_hat[idx] + return y_minus_l_hat, y_minus_g_hat, d_minus_m_hat def plr_dml1(y, x, d, g_hat, l_hat, m_hat, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - u_hat, v_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score) + y_minus_l_hat, y_minus_g_hat, d_minus_m_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls) for idx, (_, test_index) in enumerate(smpls): - thetas[idx] = plr_orth(v_hat[test_index], u_hat[test_index], d[test_index], score) + thetas[idx] = plr_orth(y_minus_l_hat[test_index], y_minus_g_hat[test_index], d_minus_m_hat[test_index], + d[test_index], score) theta_hat = np.mean(thetas) if len(smpls) > 1: - se = np.sqrt(var_plr(theta_hat, d, u_hat, v_hat, score, n_obs)) + se = np.sqrt(var_plr(theta_hat, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs)) else: assert len(smpls) == 1 test_index = smpls[0][1] n_obs = len(test_index) - se = np.sqrt(var_plr(theta_hat, d[test_index], u_hat[test_index], v_hat[test_index], + se = np.sqrt(var_plr(theta_hat, d[test_index], + y_minus_l_hat[test_index], y_minus_g_hat[test_index], d_minus_m_hat[test_index], score, n_obs)) return theta_hat, se @@ -217,31 +217,31 @@ def plr_dml1(y, x, d, g_hat, l_hat, m_hat, smpls, score): def plr_dml2(y, x, d, g_hat, l_hat, m_hat, smpls, score): n_obs = len(y) - u_hat, v_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score) - theta_hat = plr_orth(v_hat, u_hat, d, score) - se = np.sqrt(var_plr(theta_hat, d, u_hat, v_hat, score, n_obs)) + y_minus_l_hat, y_minus_g_hat, d_minus_m_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls) + theta_hat = plr_orth(y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, d, score) + se = np.sqrt(var_plr(theta_hat, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs)) return theta_hat, se -def var_plr(theta, d, u_hat, v_hat, score, n_obs): +def var_plr(theta, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs): if score == 'partialling out': - var = 1/n_obs * 1/np.power(np.mean(np.multiply(v_hat, v_hat)), 2) * \ - np.mean(np.power(np.multiply(u_hat - v_hat*theta, v_hat), 2)) + 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(v_hat, d)), 2) * \ - np.mean(np.power(np.multiply(u_hat - d*theta, v_hat), 2)) + 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(v_hat, u_hat, d, score): +def plr_orth(y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, d, score): if score == 'IV-type': - res = np.mean(np.multiply(v_hat, u_hat))/np.mean(np.multiply(v_hat, d)) + 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' - res = scipy.linalg.lstsq(v_hat.reshape(-1, 1), u_hat)[0] + res = scipy.linalg.lstsq(d_minus_m_hat.reshape(-1, 1), y_minus_l_hat)[0] return res @@ -307,27 +307,27 @@ def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, def boot_plr_single_split(theta, y, d, g_hat, l_hat, m_hat, smpls, score, se, weights, n_rep, apply_cross_fitting): - u_hat, v_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls, score) + y_minus_l_hat, y_minus_g_hat, d_minus_m_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls) if apply_cross_fitting: if score == 'partialling out': - J = np.mean(-np.multiply(v_hat, v_hat)) + J = np.mean(-np.multiply(d_minus_m_hat, d_minus_m_hat)) else: assert score == 'IV-type' - J = np.mean(-np.multiply(v_hat, d)) + J = np.mean(-np.multiply(d_minus_m_hat, d)) else: test_index = smpls[0][1] if score == 'partialling out': - J = np.mean(-np.multiply(v_hat[test_index], v_hat[test_index])) + J = np.mean(-np.multiply(d_minus_m_hat[test_index], d_minus_m_hat[test_index])) else: assert score == 'IV-type' - J = np.mean(-np.multiply(v_hat[test_index], d[test_index])) + J = np.mean(-np.multiply(d_minus_m_hat[test_index], d[test_index])) if score == 'partialling out': - psi = np.multiply(u_hat - v_hat * theta, v_hat) + psi = np.multiply(y_minus_l_hat - d_minus_m_hat * theta, d_minus_m_hat) else: assert score == 'IV-type' - psi = np.multiply(u_hat - d * theta, v_hat) + psi = np.multiply(y_minus_g_hat - d * theta, d_minus_m_hat) boot_theta, boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep, apply_cross_fitting) From d99ff13fa8d0644d122e99c5fff859e70b933965 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 21 Apr 2022 15:13:45 +0200 Subject: [PATCH 05/47] update the documentation for the PLR model --- doubleml/double_ml_plr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index d4046f35..d1fb70c8 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -16,7 +16,8 @@ class DoubleMLPLR(DoubleML): ml_g : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`g_0(X) = E[Y|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance functions :math:`l_0(X) = E[Y|X]` and + :math:`g_0(X) = E[Y - D \\theta_0|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. From f5287e62e8ec20ed0682efa7d419742809c256de Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 22 Apr 2022 09:38:25 +0200 Subject: [PATCH 06/47] fix documentation and error message --- doubleml/double_ml_plr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index d1fb70c8..04d5ca83 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -36,7 +36,7 @@ class DoubleMLPLR(DoubleML): score : str or callable A str (``'partialling out'`` or ``'IV-type'``) specifying the score function - or a callable object / function with signature ``psi_a, psi_b = score(y, d, g_hat, m_hat, smpls)``. + or a callable object / function with signature ``psi_a, psi_b = score(y, d, l_hat, g_hat, m_hat, smpls)``. Default is ``'partialling out'``. dml_procedure : str @@ -153,7 +153,7 @@ def _nuisance_est(self, smpls, n_jobs_cv): # nuisance l l_hat = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls, n_jobs=n_jobs_cv, est_params=self._get_params('ml_l'), method=self._predict_method['ml_g']) - _check_finite_predictions(l_hat, self._learner['ml_g'], 'ml_g', smpls) + _check_finite_predictions(l_hat, self._learner['ml_g'], 'ml_l', smpls) # nuisance m m_hat = _dml_cv_predict(self._learner['ml_m'], x, d, smpls=smpls, n_jobs=n_jobs_cv, From acc4da6ecaec7731fae0ae255c55a40aedaefe7f Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 22 Apr 2022 11:08:12 +0200 Subject: [PATCH 07/47] fix unit tests after refactoring in 64866dfcef988ac205755b07347df89f59129f35 --- doubleml/tests/_utils_plr_manual.py | 6 +++--- doubleml/tests/test_doubleml_exceptions.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doubleml/tests/_utils_plr_manual.py b/doubleml/tests/_utils_plr_manual.py index 4b963d7a..9e064658 100644 --- a/doubleml/tests/_utils_plr_manual.py +++ b/doubleml/tests/_utils_plr_manual.py @@ -119,7 +119,7 @@ def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, fit_g=True, m_hat = fit_predict(d, x, ml_m, m_params, smpls) if fit_g: - y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls) + y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, [], l_hat, m_hat, smpls) psi_a = -np.multiply(d_minus_m_hat, d_minus_m_hat) psi_b = np.multiply(d_minus_m_hat, y_minus_l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) @@ -141,7 +141,7 @@ def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, fit_g=True m_hat = fit_predict_proba(d, x, ml_m, m_params, smpls) if fit_g: - y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, None, l_hat, m_hat, smpls) + y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, [], l_hat, m_hat, smpls) psi_a = -np.multiply(d_minus_m_hat, d_minus_m_hat) psi_b = np.multiply(d_minus_m_hat, y_minus_l_hat) theta_initial = -np.mean(psi_b) / np.mean(psi_a) @@ -186,7 +186,7 @@ def compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls): d_minus_m_hat = np.full_like(d, np.nan, dtype='float64') for idx, (_, test_index) in enumerate(smpls): y_minus_l_hat[test_index] = y[test_index] - l_hat[idx] - if g_hat is not None: + if len(g_hat) > 0: y_minus_g_hat[test_index] = y[test_index] - g_hat[idx] d_minus_m_hat[test_index] = d[test_index] - m_hat[idx] return y_minus_l_hat, y_minus_g_hat, d_minus_m_hat diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 13c554f4..bfe349ae 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -576,10 +576,10 @@ def predict(self, X): @pytest.mark.ci def test_doubleml_nan_prediction(): - msg = r'Predictions from learner LassoWithNanPred\(\) for ml_g 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_g 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() From f5620878e82160118e407afea068264787b384b1 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 22 Apr 2022 11:19:08 +0200 Subject: [PATCH 08/47] fix unit tests after refactoring in 64866dfcef988ac205755b07347df89f59129f35 --- doubleml/tests/_utils_plr_manual.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doubleml/tests/_utils_plr_manual.py b/doubleml/tests/_utils_plr_manual.py index 9e064658..baeaefcd 100644 --- a/doubleml/tests/_utils_plr_manual.py +++ b/doubleml/tests/_utils_plr_manual.py @@ -127,7 +127,7 @@ def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, fit_g=True, ml_g = clone(learner_g) g_hat = fit_predict(y - theta_initial*d, x, ml_g, g_params, smpls) else: - g_hat = None + g_hat = [] return g_hat, l_hat, m_hat @@ -149,7 +149,7 @@ def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, fit_g=True ml_g = clone(learner_g) g_hat = fit_predict(y - theta_initial*d, x, ml_g, g_params, smpls) else: - g_hat = None + g_hat = [] return g_hat, l_hat, m_hat From bb870ccb9c953e64b546cce5f83bc0f82c8b0ff0 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 09:22:25 +0200 Subject: [PATCH 09/47] renamed ml_g to ml_l; Add additional learner ml_g for IV-type score --- doubleml/double_ml_plr.py | 115 ++++++++++++---- doubleml/tests/_utils_plr_manual.py | 128 ++++++++++-------- doubleml/tests/test_plr.py | 50 ++++--- doubleml/tests/test_plr_classifier.py | 7 +- doubleml/tests/test_plr_multi_treat.py | 9 +- doubleml/tests/test_plr_no_cross_fit.py | 59 ++++---- .../tests/test_plr_reestimate_from_scores.py | 7 +- doubleml/tests/test_plr_rep_cross.py | 9 +- .../tests/test_plr_set_ml_nuisance_pars.py | 7 +- .../tests/test_plr_set_smpls_externally.py | 7 +- doubleml/tests/test_plr_tune.py | 45 +++--- 11 files changed, 276 insertions(+), 167 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 04d5ca83..441da327 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -1,11 +1,30 @@ import numpy as np from sklearn.utils import check_X_y from sklearn.utils.multiclass import type_of_target +from sklearn.base import clone + +import warnings +from functools import wraps from .double_ml import DoubleML from ._utils import _dml_cv_predict, _dml_tune, _check_finite_predictions +def changed_api_decorator(f): + @wraps(f) + def wrapper(*args, **kwds): + ml_l_missing = (len(set(kwds).intersection({'obj_dml_data', 'ml_l', 'ml_m'})) + len(args)) < 4 + if ml_l_missing & ('ml_g' in kwds): + warnings.warn(("The required positional argument ml_g was renamed to ml_l. " + "Please adapt the argument name accordingly. " + "ml_g is redirected to ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + kwds['ml_l'] = kwds.pop('ml_g') + return f(*args, **kwds) + return wrapper + + class DoubleMLPLR(DoubleML): """Double machine learning for partially linear regression models @@ -16,8 +35,7 @@ class DoubleMLPLR(DoubleML): ml_g : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance functions :math:`l_0(X) = E[Y|X]` and - :math:`g_0(X) = E[Y - D \\theta_0|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`l_0(X) = E[Y|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. @@ -26,6 +44,11 @@ class DoubleMLPLR(DoubleML): ``predict_proba()`` can also be specified. If :py:func:`sklearn.base.is_classifier` returns ``True``, ``predict_proba()`` is used otherwise ``predict()``. + ml_g : estimator implementing ``fit()`` and ``predict()`` + A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function + :math:`g_0(X) = E[Y - D \\theta_0|X]`. + n_folds : int Number of folds. Default is ``5``. @@ -82,10 +105,12 @@ class DoubleMLPLR(DoubleML): The high-dimensional vector :math:`X = (X_1, \\ldots, X_p)` consists of other confounding covariates, and :math:`\\zeta` and :math:`V` are stochastic errors. """ + @changed_api_decorator def __init__(self, obj_dml_data, - ml_g, + ml_l, ml_m, + ml_g=None, n_folds=5, n_rep=1, score='partialling out', @@ -102,27 +127,12 @@ def __init__(self, self._check_data(self._dml_data) self._check_score(self.score) - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - ml_m_is_classifier = self._check_learner(ml_m, 'ml_m', regressor=True, classifier=True) - self._learner = {'ml_g': ml_g, 'ml_m': ml_m} - if ml_m_is_classifier: - if obj_dml_data.binary_treats.all(): - self._predict_method = {'ml_g': 'predict', '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.') - else: - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict'} - + self._check_and_set_learner(ml_l, ml_m, ml_g) self._initialize_ml_nuisance_params() def _initialize_ml_nuisance_params(self): - if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in ['ml_l', 'ml_g', 'ml_m']} - else: - self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in ['ml_l', 'ml_m']} + 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): @@ -144,6 +154,52 @@ def _check_data(self, obj_dml_data): 'To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR.') return + def _check_and_set_learner(self, ml_l, ml_m, ml_g): + _ = 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) + if isinstance(self.score, str): + if self.score == 'partialling out': + self._learner = {'ml_l': ml_l, 'ml_m': ml_m} + else: + assert self.score == 'IV-type' + if ml_g is None: + warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. " + "Set ml_g = clone(ml_l).")) + self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_g': clone(ml_l)} + else: + _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_g': ml_g} + else: + assert callable(self.score) + if ml_g is None: + self._learner = {'ml_l': ml_l, 'ml_m': ml_m} + else: + _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_g': ml_g} + + if ml_m_is_classifier: + if self._dml_data.binary_treats.all(): + self._predict_method = {'ml_l': 'predict', '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.') + else: + self._predict_method = {'ml_l': 'predict', 'ml_m': 'predict'} + if 'ml_g' in self._learner: + self._predict_method['ml_g'] = 'predict' + + return + + def set_ml_nuisance_params(self, learner, treat_var, params): + if isinstance(self.score, str) & (self.score == 'partialling out') & (learner == 'ml_g'): + warnings.warn(("learner ml_g was renamed to ml_l. " + "Please adapt the argument learner accordingly. " + "The provided parameters are set for ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + learner = 'ml_l' + super(DoubleMLPLR, self).set_ml_nuisance_params(learner, treat_var, params) + def _nuisance_est(self, smpls, n_jobs_cv): x, y = check_X_y(self._dml_data.x, self._dml_data.y, force_all_finite=False) @@ -151,9 +207,9 @@ def _nuisance_est(self, smpls, n_jobs_cv): force_all_finite=False) # nuisance l - l_hat = _dml_cv_predict(self._learner['ml_g'], x, y, smpls=smpls, n_jobs=n_jobs_cv, - est_params=self._get_params('ml_l'), method=self._predict_method['ml_g']) - _check_finite_predictions(l_hat, self._learner['ml_g'], '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, 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, @@ -171,7 +227,7 @@ def _nuisance_est(self, smpls, n_jobs_cv): # an estimate of g is obtained for the IV-type score and callable scores g_hat = None - if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): + if 'ml_g' in self._learner: # get an initial estimate for theta using the partialling out score psi_a = -np.multiply(d - m_hat, d - m_hat) psi_b = np.multiply(d - m_hat, y - l_hat) @@ -215,12 +271,13 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_g': None, - 'ml_m': 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_g'], param_grids['ml_g'], scoring_methods['ml_g'], + 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'], @@ -230,7 +287,7 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ 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 (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): + 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) diff --git a/doubleml/tests/_utils_plr_manual.py b/doubleml/tests/_utils_plr_manual.py index baeaefcd..6b569062 100644 --- a/doubleml/tests/_utils_plr_manual.py +++ b/doubleml/tests/_utils_plr_manual.py @@ -6,24 +6,24 @@ from ._utils import fit_predict, fit_predict_proba, tune_grid_search -def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, l_params=None, m_params=None, +def fit_plr_multitreat(y, x, d, learner_l, learner_m, learner_g, all_smpls, dml_procedure, 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] thetas = list() ses = list() - all_g_hat = list() all_l_hat = list() all_m_hat = list() + all_g_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] thetas_this_rep = np.full(n_d, np.nan) ses_this_rep = np.full(n_d, np.nan) - all_g_hat_this_rep = list() all_l_hat_this_rep = list() all_m_hat_this_rep = list() + all_g_hat_this_rep = list() for i_d in range(n_d): if use_other_treat_as_covariate: @@ -31,17 +31,20 @@ def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, else: xd = x - g_hat, l_hat, m_hat, thetas_this_rep[i_d], ses_this_rep[i_d] = fit_plr_single_split( - y, xd, d[:, i_d], learner_g, learner_m, smpls, dml_procedure, score, g_params, l_params, m_params) - all_g_hat_this_rep.append(g_hat) + 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, dml_procedure, 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) thetas.append(thetas_this_rep) ses.append(ses_this_rep) - all_g_hat.append(all_g_hat_this_rep) all_l_hat.append(all_l_hat_this_rep) all_m_hat.append(all_m_hat_this_rep) + all_g_hat.append(all_g_hat_this_rep) theta = np.full(n_d, np.nan) se = np.full(n_d, np.nan) @@ -53,73 +56,78 @@ def fit_plr_multitreat(y, x, d, learner_g, learner_m, all_smpls, dml_procedure, res = {'theta': theta, 'se': se, 'thetas': thetas, 'ses': ses, - 'all_g_hat': all_g_hat, 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat} + '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_g, learner_m, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, l_params=None, m_params=None): +def fit_plr(y, x, d, learner_l, learner_m, learner_g, all_smpls, dml_procedure, score, + n_rep=1, l_params=None, m_params=None, g_params=None): n_obs = len(y) thetas = np.zeros(n_rep) ses = np.zeros(n_rep) - all_g_hat = list() all_l_hat = list() all_m_hat = list() + all_g_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, l_hat, m_hat, thetas[i_rep], ses[i_rep] = fit_plr_single_split( - y, x, d, learner_g, learner_m, smpls, dml_procedure, score, g_params, l_params, m_params) - all_g_hat.append(g_hat) + 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, dml_procedure, 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) 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': all_g_hat, 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat} + '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_g, learner_m, smpls, dml_procedure, score, - g_params=None, l_params=None, m_params=None): +def fit_plr_single_split(y, x, d, learner_l, learner_m, learner_g, smpls, dml_procedure, score, + l_params=None, m_params=None, g_params=None): fit_g = (score == 'IV-type') | callable(score) if is_classifier(learner_m): - g_hat, l_hat, m_hat = fit_nuisance_plr_classifier(y, x, d, - learner_g, learner_m, smpls, fit_g, - g_params, l_params, m_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: - g_hat, l_hat, m_hat = fit_nuisance_plr(y, x, d, - learner_g, learner_m, smpls, fit_g, - g_params, l_params, m_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) if dml_procedure == 'dml1': theta, se = plr_dml1(y, x, d, - g_hat, l_hat, m_hat, + l_hat, m_hat, g_hat, smpls, score) else: assert dml_procedure == 'dml2' theta, se = plr_dml2(y, x, d, - g_hat, l_hat, m_hat, + l_hat, m_hat, g_hat, smpls, score) - return g_hat, l_hat, m_hat, theta, se + return l_hat, m_hat, g_hat, theta, se -def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, fit_g=True, - g_params=None, l_params=None, m_params=None): - ml_l = clone(learner_g) +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) ml_m = clone(learner_m) m_hat = fit_predict(d, x, ml_m, m_params, smpls) if fit_g: - y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, [], l_hat, m_hat, smpls) + y_minus_l_hat, d_minus_m_hat, _ = compute_plr_residuals(y, d, l_hat, m_hat, [], smpls) psi_a = -np.multiply(d_minus_m_hat, d_minus_m_hat) psi_b = np.multiply(d_minus_m_hat, y_minus_l_hat) theta_initial = -np.nanmean(psi_b) / np.nanmean(psi_a) @@ -129,19 +137,19 @@ def fit_nuisance_plr(y, x, d, learner_g, learner_m, smpls, fit_g=True, else: g_hat = [] - return g_hat, l_hat, m_hat + return l_hat, m_hat, g_hat -def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, fit_g=True, - g_params=None, l_params=None, m_params=None): - ml_l = clone(learner_g) +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) ml_m = clone(learner_m) m_hat = fit_predict_proba(d, x, ml_m, m_params, smpls) if fit_g: - y_minus_l_hat, _, d_minus_m_hat = compute_plr_residuals(y, d, [], l_hat, m_hat, smpls) + y_minus_l_hat, d_minus_m_hat, _ = compute_plr_residuals(y, d, l_hat, m_hat, [], smpls) psi_a = -np.multiply(d_minus_m_hat, d_minus_m_hat) psi_b = np.multiply(d_minus_m_hat, y_minus_l_hat) theta_initial = -np.mean(psi_b) / np.mean(psi_a) @@ -151,11 +159,11 @@ def fit_nuisance_plr_classifier(y, x, d, learner_g, learner_m, smpls, fit_g=True else: g_hat = [] - return g_hat, l_hat, m_hat + return l_hat, m_hat, g_hat -def tune_nuisance_plr(y, x, d, ml_g, ml_m, smpls, n_folds_tune, param_grid_g, param_grid_m, tune_g=True): - l_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune) +def tune_nuisance_plr(y, x, d, ml_l, ml_m, ml_g, smpls, n_folds_tune, param_grid_l, param_grid_m, 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(d, x, ml_m, smpls, param_grid_m, n_folds_tune) @@ -177,54 +185,54 @@ def tune_nuisance_plr(y, x, d, ml_g, ml_m, smpls, n_folds_tune, param_grid_g, pa l_best_params = [xx.best_params_ for xx in l_tune_res] m_best_params = [xx.best_params_ for xx in m_tune_res] - return g_best_params, l_best_params, m_best_params + return l_best_params, m_best_params, g_best_params -def compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls): +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') - y_minus_g_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: y_minus_g_hat[test_index] = y[test_index] - g_hat[idx] d_minus_m_hat[test_index] = d[test_index] - m_hat[idx] - return y_minus_l_hat, y_minus_g_hat, d_minus_m_hat + return y_minus_l_hat, d_minus_m_hat, y_minus_g_hat -def plr_dml1(y, x, d, g_hat, l_hat, m_hat, smpls, score): +def plr_dml1(y, x, d, l_hat, m_hat, g_hat, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - y_minus_l_hat, y_minus_g_hat, d_minus_m_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls) + y_minus_l_hat, d_minus_m_hat, y_minus_g_hat = compute_plr_residuals(y, d, l_hat, m_hat, g_hat, smpls) for idx, (_, test_index) in enumerate(smpls): - thetas[idx] = plr_orth(y_minus_l_hat[test_index], y_minus_g_hat[test_index], d_minus_m_hat[test_index], + thetas[idx] = plr_orth(y_minus_l_hat[test_index], d_minus_m_hat[test_index], y_minus_g_hat[test_index], d[test_index], score) theta_hat = np.mean(thetas) if len(smpls) > 1: - se = np.sqrt(var_plr(theta_hat, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs)) + se = np.sqrt(var_plr(theta_hat, d, y_minus_l_hat, d_minus_m_hat, y_minus_g_hat, score, n_obs)) else: assert len(smpls) == 1 test_index = smpls[0][1] n_obs = len(test_index) se = np.sqrt(var_plr(theta_hat, d[test_index], - y_minus_l_hat[test_index], y_minus_g_hat[test_index], d_minus_m_hat[test_index], + y_minus_l_hat[test_index], d_minus_m_hat[test_index], y_minus_g_hat[test_index], score, n_obs)) return theta_hat, se -def plr_dml2(y, x, d, g_hat, l_hat, m_hat, smpls, score): +def plr_dml2(y, x, d, l_hat, m_hat, g_hat, smpls, score): n_obs = len(y) - y_minus_l_hat, y_minus_g_hat, d_minus_m_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls) - theta_hat = plr_orth(y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, d, score) - se = np.sqrt(var_plr(theta_hat, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs)) + y_minus_l_hat, d_minus_m_hat, y_minus_g_hat = compute_plr_residuals(y, d, l_hat, m_hat, g_hat, smpls) + theta_hat = plr_orth(y_minus_l_hat, d_minus_m_hat, y_minus_g_hat, d, score) + se = np.sqrt(var_plr(theta_hat, d, y_minus_l_hat, d_minus_m_hat, y_minus_g_hat, score, n_obs)) return theta_hat, se -def var_plr(theta, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs): +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)) @@ -236,7 +244,7 @@ def var_plr(theta, d, y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, score, n_obs) return var -def plr_orth(y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, d, score): +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)) else: @@ -246,7 +254,7 @@ def plr_orth(y_minus_l_hat, y_minus_g_hat, d_minus_m_hat, d, score): return res -def boot_plr(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, +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_theta = list() @@ -261,7 +269,7 @@ def boot_plr(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_theta, boot_t_stat = boot_plr_single_split( - thetas[i_rep], y, d, all_g_hat[i_rep], all_l_hat[i_rep], all_m_hat[i_rep], smpls, + 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_theta.append(boot_theta) @@ -273,7 +281,7 @@ def boot_plr(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, return boot_theta, boot_t_stat -def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, +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] @@ -293,7 +301,7 @@ def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, for i_d in range(n_d): boot_theta[i_d, :], boot_t_stat[i_d, :] = boot_plr_single_split( thetas[i_rep][i_d], y, d[:, i_d], - all_g_hat[i_rep][i_d], all_l_hat[i_rep][i_d], all_m_hat[i_rep][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) all_boot_theta.append(boot_theta) @@ -305,9 +313,9 @@ def boot_plr_multitreat(y, d, thetas, ses, all_g_hat, all_l_hat, all_m_hat, return boot_theta, boot_t_stat -def boot_plr_single_split(theta, y, d, g_hat, l_hat, m_hat, +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, y_minus_g_hat, d_minus_m_hat = compute_plr_residuals(y, d, g_hat, l_hat, m_hat, smpls) + 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': diff --git a/doubleml/tests/test_plr.py b/doubleml/tests/test_plr.py index 4c6b6813..7b8b56d9 100644 --- a/doubleml/tests/test_plr.py +++ b/doubleml/tests/test_plr.py @@ -45,16 +45,25 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure): x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - dml_procedure=dml_procedure) + if score == 'partialling out': + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, + ml_l, ml_m, + n_folds=n_folds, + score=score, + dml_procedure=dml_procedure) + else: + assert score == 'IV-type' + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, + ml_l, ml_m, ml_g, + n_folds, + score=score, + dml_procedure=dml_procedure) dml_plr_obj.fit() @@ -65,7 +74,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure): n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_plr(y, x, d, clone(learner), clone(learner), + res_manual = fit_plr(y, x, d, clone(learner), clone(learner), clone(learner), all_smpls, dml_procedure, score) res_dict = {'coef': dml_plr_obj.coef, @@ -77,7 +86,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + 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) @@ -127,15 +136,24 @@ def dml_plr_ols_manual_fixture(generate_data1, score, dml_procedure): 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) - dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, - n_folds, - score=score, - dml_procedure=dml_procedure) + if score == 'partialling out': + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, + ml_l, ml_m, + n_folds=n_folds, + score=score, + dml_procedure=dml_procedure) + else: + assert score == 'IV-type' + dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, + ml_l, ml_m, ml_g, + n_folds, + score=score, + dml_procedure=dml_procedure) n = data.shape[0] this_smpl = list() @@ -183,12 +201,12 @@ def dml_plr_ols_manual_fixture(generate_data1, score, dml_procedure): if dml_procedure == 'dml1': res_manual, se_manual = plr_dml1(y, x, d, - g_hat, l_hat, m_hat, + l_hat, m_hat, g_hat, smpls, score) else: assert dml_procedure == 'dml2' res_manual, se_manual = plr_dml2(y, x, d, - g_hat, l_hat, m_hat, + l_hat, m_hat, g_hat, smpls, score) res_dict = {'coef': dml_plr_obj.coef, @@ -200,7 +218,7 @@ def dml_plr_ols_manual_fixture(generate_data1, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, [res_manual], [se_manual], - [g_hat], [l_hat], [m_hat], + [l_hat], [m_hat], [g_hat], [smpls], score, bootstrap, n_rep_boot) np.random.seed(3141) diff --git a/doubleml/tests/test_plr_classifier.py b/doubleml/tests/test_plr_classifier.py index d8aa037f..c40e3ffe 100644 --- a/doubleml/tests/test_plr_classifier.py +++ b/doubleml/tests/test_plr_classifier.py @@ -43,12 +43,13 @@ def dml_plr_binary_classifier_fixture(learner, score, dml_procedure): n_rep_boot = 502 # Set machine learning methods for m & g + ml_l = Lasso(alpha=0.3) ml_g = Lasso() ml_m = clone(learner) np.random.seed(3141) dml_plr_obj = dml.DoubleMLPLR(bonus_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, score=score, dml_procedure=dml_procedure) @@ -62,7 +63,7 @@ def dml_plr_binary_classifier_fixture(learner, score, dml_procedure): n_obs = len(y) all_smpls = draw_smpls(n_obs, n_folds) - res_manual = fit_plr(y, x, d, clone(ml_g), clone(ml_m), + res_manual = fit_plr(y, x, d, clone(ml_l), clone(ml_m), clone(ml_g), all_smpls, dml_procedure, score) res_dict = {'coef': dml_plr_obj.coef, @@ -74,7 +75,7 @@ def dml_plr_binary_classifier_fixture(learner, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + 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) diff --git a/doubleml/tests/test_plr_multi_treat.py b/doubleml/tests/test_plr_multi_treat.py index 299023c7..bda58d2b 100644 --- a/doubleml/tests/test_plr_multi_treat.py +++ b/doubleml/tests/test_plr_multi_treat.py @@ -60,13 +60,14 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, d_cols = data.columns[data.columns.str.startswith('d')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', d_cols, x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, n_rep, score=score, dml_procedure=dml_procedure) @@ -81,7 +82,7 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, all_smpls = draw_smpls(n_obs, n_folds, n_rep) res_manual = fit_plr_multitreat(y, x, d, - clone(learner), clone(learner), + clone(learner), clone(learner), clone(learner), all_smpls, dml_procedure, score, n_rep=n_rep) @@ -96,7 +97,7 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, boot_theta, boot_t_stat = boot_plr_multitreat( y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], all_smpls, score, bootstrap, n_rep_boot, n_rep) diff --git a/doubleml/tests/test_plr_no_cross_fit.py b/doubleml/tests/test_plr_no_cross_fit.py index 393b9a83..c0ef58a9 100644 --- a/doubleml/tests/test_plr_no_cross_fit.py +++ b/doubleml/tests/test_plr_no_cross_fit.py @@ -41,13 +41,14 @@ def dml_plr_no_cross_fit_fixture(generate_data1, learner, score, n_folds): x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, score=score, dml_procedure=dml_procedure, @@ -67,7 +68,7 @@ def dml_plr_no_cross_fit_fixture(generate_data1, learner, score, n_folds): smpls = all_smpls[0] smpls = [smpls[0]] - res_manual = fit_plr(y, x, d, clone(learner), clone(learner), + res_manual = fit_plr(y, x, d, clone(learner), clone(learner), clone(learner), [smpls], dml_procedure, score) res_dict = {'coef': dml_plr_obj.coef, @@ -79,7 +80,7 @@ def dml_plr_no_cross_fit_fixture(generate_data1, learner, score, n_folds): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], [smpls], score, bootstrap, n_rep_boot, apply_cross_fitting=False) @@ -133,13 +134,14 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, n_rep, score, @@ -160,21 +162,21 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): thetas = np.zeros(n_rep) ses = np.zeros(n_rep) - all_g_hat = list() all_l_hat = list() all_m_hat = list() + all_g_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, l_hat, m_hat = fit_nuisance_plr(y, x, d, - clone(learner), clone(learner), smpls) + l_hat, m_hat, g_hat = fit_nuisance_plr(y, x, d, + clone(learner), clone(learner), clone(learner), smpls) - all_g_hat.append(g_hat) all_l_hat.append(l_hat) all_m_hat.append(m_hat) + all_g_hat.append(g_hat) thetas[i_rep], ses[i_rep] = plr_dml1(y, x, d, - all_g_hat[i_rep], all_l_hat[i_rep], all_m_hat[i_rep], + all_l_hat[i_rep], all_m_hat[i_rep], all_g_hat[i_rep], smpls, score) res_manual = np.median(thetas) @@ -190,7 +192,7 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, thetas, ses, - all_g_hat, all_l_hat, all_m_hat, + all_l_hat, all_m_hat, all_g_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=n_rep, apply_cross_fitting=False) @@ -237,8 +239,10 @@ def tune_on_folds(request): @pytest.fixture(scope="module") def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_folds): - par_grid = {'ml_g': {'alpha': np.linspace(0.05, .95, 7)}, + par_grid = {'ml_l': {'alpha': np.linspace(0.05, .95, 7)}, 'ml_m': {'alpha': np.linspace(0.05, .95, 7)}} + if score == 'IV-type': + par_grid['ml_g'] = {'alpha': np.linspace(0.05, .95, 7)} n_folds_tune = 3 boot_methods = ['normal'] @@ -250,13 +254,14 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = Lasso() + ml_l = Lasso() ml_m = Lasso() + ml_g = Lasso() np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds=2, score=score, dml_procedure=dml_procedure, @@ -278,22 +283,26 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo smpls = all_smpls[0] smpls = [smpls[0]] - tune_g = score == 'IV-type' + tune_g = (score == 'IV-type') + if not tune_g: + par_grid['ml_g'] = None if tune_on_folds: - g_params, l_params, m_params = tune_nuisance_plr(y, x, d, - clone(ml_g), clone(ml_m), smpls, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], + l_params, m_params, g_params = tune_nuisance_plr(y, x, d, + clone(ml_l), clone(ml_m), clone(ml_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([]))] - g_params, l_params, m_params = tune_nuisance_plr(y, x, d, - clone(ml_g), clone(ml_m), xx, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], + l_params, m_params, g_params = tune_nuisance_plr(y, x, d, + clone(ml_l), clone(ml_m), clone(ml_g), + xx, n_folds_tune, + par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_g'], tune_g) - res_manual = fit_plr(y, x, d, clone(ml_m), clone(ml_g), + res_manual = fit_plr(y, x, d, clone(ml_l), clone(ml_m), clone(ml_g), [smpls], dml_procedure, score, - g_params=g_params, l_params=l_params, m_params=m_params) + l_params=l_params, m_params=m_params, g_params=g_params) res_dict = {'coef': dml_plr_obj.coef, 'coef_manual': res_manual['theta'], @@ -304,7 +313,7 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_g_hat'], [smpls], score, bootstrap, n_rep_boot, apply_cross_fitting=False) diff --git a/doubleml/tests/test_plr_reestimate_from_scores.py b/doubleml/tests/test_plr_reestimate_from_scores.py index 6a4b44bd..80fcad37 100644 --- a/doubleml/tests/test_plr_reestimate_from_scores.py +++ b/doubleml/tests/test_plr_reestimate_from_scores.py @@ -41,13 +41,14 @@ def dml_plr_reestimate_fixture(generate_data1, learner, score, dml_procedure, n_ x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, n_rep, score, @@ -56,7 +57,7 @@ def dml_plr_reestimate_fixture(generate_data1, learner, score, dml_procedure, n_ np.random.seed(3141) dml_plr_obj2 = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, n_rep, score, diff --git a/doubleml/tests/test_plr_rep_cross.py b/doubleml/tests/test_plr_rep_cross.py index 391ac37b..1650dbf2 100644 --- a/doubleml/tests/test_plr_rep_cross.py +++ b/doubleml/tests/test_plr_rep_cross.py @@ -49,13 +49,14 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure, n_rep): x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, n_rep, score, @@ -70,7 +71,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure, n_rep): 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), + res_manual = fit_plr(y, x, d, clone(learner), clone(learner), clone(learner), all_smpls, dml_procedure, score, n_rep) res_dict = {'coef': dml_plr_obj.coef, @@ -83,7 +84,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure, n_rep): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + 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) diff --git a/doubleml/tests/test_plr_set_ml_nuisance_pars.py b/doubleml/tests/test_plr_set_ml_nuisance_pars.py index e61b5abe..c80e6c2a 100644 --- a/doubleml/tests/test_plr_set_ml_nuisance_pars.py +++ b/doubleml/tests/test_plr_set_ml_nuisance_pars.py @@ -32,13 +32,14 @@ def dml_plr_fixture(generate_data1, score, dml_procedure): alpha = 0.05 learner = Lasso(alpha=alpha) # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d']) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, score=score, dml_procedure=dml_procedure) @@ -52,7 +53,7 @@ def dml_plr_fixture(generate_data1, score, dml_procedure): ml_m = clone(learner) dml_plr_obj_ext_set_par = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, score=score, dml_procedure=dml_procedure) diff --git a/doubleml/tests/test_plr_set_smpls_externally.py b/doubleml/tests/test_plr_set_smpls_externally.py index c745362f..c236d8a9 100644 --- a/doubleml/tests/test_plr_set_smpls_externally.py +++ b/doubleml/tests/test_plr_set_smpls_externally.py @@ -41,13 +41,14 @@ def dml_plr_smpls_fixture(generate_data1, learner, score, dml_procedure, n_rep): x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for m & g - ml_g = clone(learner) + ml_l = clone(learner) ml_m = clone(learner) + ml_g = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, n_rep, score, @@ -58,7 +59,7 @@ def dml_plr_smpls_fixture(generate_data1, learner, score, dml_procedure, n_rep): smpls = dml_plr_obj.smpls dml_plr_obj2 = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, score=score, dml_procedure=dml_procedure, draw_sample_splitting=False) diff --git a/doubleml/tests/test_plr_tune.py b/doubleml/tests/test_plr_tune.py index 3967b0dd..1af5f0b3 100644 --- a/doubleml/tests/test_plr_tune.py +++ b/doubleml/tests/test_plr_tune.py @@ -15,7 +15,7 @@ @pytest.fixture(scope='module', params=[Lasso(), ElasticNet()]) -def learner_g(request): +def learner_l(request): return request.param @@ -27,7 +27,14 @@ def learner_m(request): @pytest.fixture(scope='module', - params=['partialling out']) + params=[Lasso(), + ElasticNet()]) +def learner_g(request): + return request.param + + +@pytest.fixture(scope='module', + params=['partialling out', 'IV-type']) def score(request): return request.param @@ -54,9 +61,10 @@ def get_par_grid(learner): @pytest.fixture(scope="module") -def dml_plr_fixture(generate_data2, learner_g, learner_m, score, dml_procedure, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), - 'ml_m': get_par_grid(learner_m)} +def dml_plr_fixture(generate_data2, learner_l, learner_m, learner_g, score, dml_procedure, 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)} n_folds_tune = 4 boot_methods = ['normal'] @@ -67,12 +75,13 @@ def dml_plr_fixture(generate_data2, learner_g, learner_m, score, dml_procedure, obj_dml_data = generate_data2 # Set machine learning methods for m & g - ml_g = clone(learner_g) + ml_l = clone(learner_l) ml_m = clone(learner_m) + ml_g = clone(learner_g) np.random.seed(3141) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, ml_g, n_folds, score=score, dml_procedure=dml_procedure) @@ -91,23 +100,25 @@ def dml_plr_fixture(generate_data2, learner_g, learner_m, score, dml_procedure, 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: - g_params, l_params, m_params = tune_nuisance_plr(y, x, d, - clone(learner_g), clone(learner_m), smpls, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], 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([]))] - g_params, l_params, m_params = tune_nuisance_plr(y, x, d, - clone(learner_g), clone(learner_m), xx, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], 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_g), clone(learner_m), + res_manual = fit_plr(y, x, d, clone(learner_l), clone(learner_m), clone(learner_g), all_smpls, dml_procedure, score, - g_params=g_params, l_params=l_params, m_params=m_params) + l_params=l_params, m_params=m_params, g_params=g_params) res_dict = {'coef': dml_plr_obj.coef, 'coef_manual': res_manual['theta'], @@ -118,7 +129,7 @@ def dml_plr_fixture(generate_data2, learner_g, learner_m, score, dml_procedure, for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_plr(y, d, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_l_hat'], res_manual['all_m_hat'], + 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) From 4e6ecafa320e164d537bb6407052e14b8e13ca3b Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 09:31:11 +0200 Subject: [PATCH 10/47] update documentation --- doubleml/double_ml_plr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 441da327..1765bfdb 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -33,7 +33,7 @@ class DoubleMLPLR(DoubleML): obj_dml_data : :class:`DoubleMLData` object The :class:`DoubleMLData` object providing the data and specifying the variables for the causal model. - ml_g : estimator implementing ``fit()`` and ``predict()`` + ml_l : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`l_0(X) = E[Y|X]`. @@ -48,6 +48,8 @@ class DoubleMLPLR(DoubleML): A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`g_0(X) = E[Y - D \\theta_0|X]`. + Note: The learner `ml_g` is only required for the score ``'IV-type'``. Optionally, it can be specified and + estimated for callable scores. n_folds : int Number of folds. From 81edf45735c2f4d79c85519e9d109d6748943048 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 09:34:54 +0200 Subject: [PATCH 11/47] simplify code to set learner and predict_method --- doubleml/double_ml_plr.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 1765bfdb..2009eac3 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -159,34 +159,30 @@ def _check_data(self, obj_dml_data): def _check_and_set_learner(self, ml_l, ml_m, ml_g): _ = 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) - if isinstance(self.score, str): - if self.score == 'partialling out': - self._learner = {'ml_l': ml_l, 'ml_m': ml_m} + self._learner = {'ml_l': ml_l, 'ml_m': ml_m} + if isinstance(self.score, str) & (self.score == 'IV-type'): + if ml_g is None: + 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) else: - assert self.score == 'IV-type' - if ml_g is None: - warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. " - "Set ml_g = clone(ml_l).")) - self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_g': clone(ml_l)} - else: - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_g': ml_g} + _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + self._learner['ml_g'] = ml_g else: assert callable(self.score) - if ml_g is None: - self._learner = {'ml_l': ml_l, 'ml_m': ml_m} - else: + if ml_g is not None: _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_g': ml_g} + self._learner['ml_g'] = ml_g + self._predict_method = {'ml_l': 'predict'} if ml_m_is_classifier: if self._dml_data.binary_treats.all(): - self._predict_method = {'ml_l': 'predict', '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.') else: - self._predict_method = {'ml_l': 'predict', 'ml_m': 'predict'} + self._predict_method['ml_m'] = 'predict' if 'ml_g' in self._learner: self._predict_method['ml_g'] = 'predict' From 405dc5852ce864833d5cc4768b6af83f1281fe50 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 09:55:45 +0200 Subject: [PATCH 12/47] additional changes for the new API with ml_g and ml_l --- doubleml/double_ml_plr.py | 3 +-- doubleml/tests/test_doubleml_exceptions.py | 27 ++++++++++++---------- doubleml/tests/test_doubleml_scores.py | 5 +++- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 2009eac3..f7f2ca7a 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -169,8 +169,7 @@ def _check_and_set_learner(self, ml_l, ml_m, ml_g): _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) self._learner['ml_g'] = ml_g else: - assert callable(self.score) - if ml_g is not None: + if callable(self.score) & (ml_g is not None): _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) self._learner['ml_g'] = ml_g diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index bfe349ae..907a44a8 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -11,11 +11,12 @@ np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=10) -ml_g = Lasso() +ml_l = Lasso() ml_m = Lasso() +ml_g = Lasso() ml_r = Lasso() -dml_plr = DoubleMLPLR(dml_data, ml_g, ml_m) -dml_plr_iv_type = DoubleMLPLR(dml_data, ml_g, ml_m, score='IV-type') +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_data_irm = make_irm_data(n_obs=10) dml_data_iivm = make_iivm_data(n_obs=10) @@ -225,7 +226,7 @@ def test_doubleml_exception_get_params(): 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_g or ml_m.' + 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') @@ -330,18 +331,20 @@ 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_g and ml_m' + # TODO Add tests with IV-type score + + 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_g': {'alpha': \[0.05, 0.5\]}}. " - "param_grids must be a dictionary with keys ml_g 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_g': {'alpha': [0.05, 0.5]}}) + dml_plr.tune({'ml_r': {'alpha': [0.05, 0.5]}}) - param_grids = {'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_g and ml_m.') + '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') @@ -421,8 +424,8 @@ def predict(self, X): @pytest.mark.ci def test_doubleml_exception_learner(): - err_msg_prefix = 'Invalid learner provided for ml_g: ' - warn_msg_prefix = 'Learner provided for ml_g 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.' with pytest.raises(TypeError, match=msg): diff --git a/doubleml/tests/test_doubleml_scores.py b/doubleml/tests/test_doubleml_scores.py index b249e278..bfccce58 100644 --- a/doubleml/tests/test_doubleml_scores.py +++ b/doubleml/tests/test_doubleml_scores.py @@ -64,8 +64,11 @@ def test_plr_callable_vs_str_score(): def test_plr_callable_vs_pred_export(): preds = dml_plr_callable_score.predictions l_hat = preds['ml_l'].squeeze() - g_hat = preds['ml_g'].squeeze() m_hat = preds['ml_m'].squeeze() + if 'ml_g' in preds: + g_hat = preds['ml_g'].squeeze() + else: + g_hat = None psi_a, psi_b = plr_score(dml_data_plr.y, dml_data_plr.d, l_hat, g_hat, m_hat, dml_plr_callable_score.smpls[0]) From 93ad699170b997e3c6a525de3d562d5c7bd97af1 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 11:42:29 +0200 Subject: [PATCH 13/47] fix deprecation warning and add corresponding unit tests --- doubleml/double_ml_plr.py | 37 ++++++++++++++++++++- doubleml/tests/test_doubleml_exceptions.py | 38 +++++++++++++++++----- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index f7f2ca7a..62f2db64 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -10,6 +10,7 @@ from ._utils import _dml_cv_predict, _dml_tune, _check_finite_predictions +# To be removed in version 0.6.0 def changed_api_decorator(f): @wraps(f) def wrapper(*args, **kwds): @@ -187,9 +188,10 @@ def _check_and_set_learner(self, ml_l, ml_m, ml_g): return + # To be removed in version 0.6.0 def set_ml_nuisance_params(self, learner, treat_var, params): if isinstance(self.score, str) & (self.score == 'partialling out') & (learner == 'ml_g'): - warnings.warn(("learner ml_g was renamed to ml_l. " + warnings.warn(("Learner ml_g was renamed to ml_l. " "Please adapt the argument learner accordingly. " "The provided parameters are set for ml_l. " "The redirection will be removed in a future version."), @@ -260,6 +262,39 @@ def _score_elements(self, y, d, l_hat, g_hat, m_hat, smpls): return psi_a, psi_b + # To be removed in version 0.6.0 + 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): + + if isinstance(self.score, str) and (self.score == 'partialling out') and (param_grids is not None) and \ + ('ml_g' in param_grids) and ('ml_l' not in param_grids): + warnings.warn(("Learner ml_g was renamed to ml_l. " + "Please adapt the key of param_grids accordingly. " + "The provided param_grids for ml_g are set for ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + param_grids['ml_l'] = param_grids.pop('ml_g') + + if isinstance(self.score, str) and (self.score == 'partialling out') and (scoring_methods is not None) and \ + ('ml_g' in scoring_methods) and ('ml_l' not in scoring_methods): + warnings.warn(("Learner ml_g was renamed to ml_l. " + "Please adapt the key of scoring_methods accordingly. " + "The provided scoring_methods for ml_g are set for ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + scoring_methods['ml_l'] = scoring_methods.pop('ml_g') + + super(DoubleMLPLR, self).tune(param_grids, tune_on_folds, scoring_methods, n_folds_tune, search_mode, + n_iter_randomized_search, n_jobs_cv, set_as_params, return_tune_res) + 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, diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 907a44a8..6772458f 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -3,7 +3,7 @@ import numpy as np from doubleml import DoubleMLPLR, DoubleMLIRM, DoubleMLIIVM, DoubleMLPLIV, DoubleMLData, DoubleMLClusterData -from doubleml.datasets import make_plr_CCDDHNR2018, make_irm_data, make_pliv_CHS2015, make_iivm_data,\ +from doubleml.datasets import make_plr_CCDDHNR2018, make_irm_data, make_pliv_CHS2015, make_iivm_data, \ make_pliv_multiway_cluster_CKMS2021 from sklearn.linear_model import Lasso, LogisticRegression @@ -57,7 +57,7 @@ def test_doubleml_exception_data(): 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'), @@ -231,8 +231,6 @@ def test_doubleml_exception_get_params(): dml_plr_iv_type.get_params('ml_r') - - @pytest.mark.ci def test_doubleml_exception_smpls(): msg = ('Sample splitting not specified. ' @@ -330,9 +328,6 @@ def test_doubleml_exception_p_adjust(): @pytest.mark.ci def test_doubleml_exception_tune(): - - # TODO Add tests with IV-type score - 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]) @@ -341,6 +336,25 @@ def test_doubleml_exception_tune(): with pytest.raises(ValueError, match=msg): 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' + 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.") + 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]}}) + + msg = 'Learner ml_g was renamed to ml_l. ' + with pytest.warns(DeprecationWarning, match=msg): + dml_plr.tune({'ml_g': {'alpha': [0.05, 0.5]}, + 'ml_m': {'alpha': [0.05, 0.5]}}) + with pytest.warns(DeprecationWarning, match=msg): + dml_plr.tune({'ml_l': {'alpha': [0.05, 0.5]}, + 'ml_m': {'alpha': [0.05, 0.5]}}, + scoring_methods={'ml_g': 'explained_variance', + 'ml_m': 'explained_variance'}) + 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. ' @@ -386,7 +400,6 @@ def test_doubleml_exception_tune(): @pytest.mark.ci def test_doubleml_exception_set_ml_nuisance_params(): - 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}) @@ -456,6 +469,10 @@ def test_doubleml_exception_learner(): with pytest.raises(ValueError, match=msg): _ = DoubleMLPLR(dml_data, Lasso(), LogisticRegression()) + msg = 'ml_g was renamed to ml_l' + with pytest.warns(DeprecationWarning, match=msg): + _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) + # 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.') @@ -515,6 +532,10 @@ def test_doubleml_exception_learner(): dml_iivm_hidden_classifier.set_ml_nuisance_params('ml_g0', 'd', {'max_iter': 314}) dml_iivm_hidden_classifier.fit() + msg = 'Learner ml_g was renamed to ml_l. ' + with pytest.warns(DeprecationWarning, match=msg): + dml_plr.set_ml_nuisance_params('ml_g', 'd', {'max_iter': 314}) + @pytest.mark.ci @pytest.mark.filterwarnings("ignore:Learner provided for") @@ -578,7 +599,6 @@ def predict(self, X): @pytest.mark.ci def test_doubleml_nan_prediction(): - 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() From 43a74bbdfd7e3d47707fdcc281419110e7871718 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 11:57:32 +0200 Subject: [PATCH 14/47] change order of predictions to be consistently l_hat, m_hat, g_hat --- doubleml/double_ml_plr.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 62f2db64..eb7dd888 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -62,7 +62,7 @@ class DoubleMLPLR(DoubleML): score : str or callable A str (``'partialling out'`` or ``'IV-type'``) specifying the score function - or a callable object / function with signature ``psi_a, psi_b = score(y, d, l_hat, g_hat, m_hat, smpls)``. + or a callable object / function with signature ``psi_a, psi_b = score(y, d, l_hat, m_hat, g_hat, smpls)``. Default is ``'partialling out'``. dml_procedure : str @@ -236,14 +236,14 @@ def _nuisance_est(self, smpls, n_jobs_cv): est_params=self._get_params('ml_g'), method=self._predict_method['ml_g']) _check_finite_predictions(g_hat, self._learner['ml_g'], 'ml_g', smpls) - psi_a, psi_b = self._score_elements(y, d, l_hat, g_hat, m_hat, smpls) - preds = {'ml_g': g_hat, - 'ml_l': l_hat, - 'ml_m': m_hat} + psi_a, psi_b = self._score_elements(y, d, l_hat, m_hat, g_hat, smpls) + preds = {'ml_l': l_hat, + 'ml_m': m_hat, + 'ml_g': g_hat} return psi_a, psi_b, preds - def _score_elements(self, y, d, l_hat, g_hat, m_hat, smpls): + def _score_elements(self, y, d, l_hat, m_hat, g_hat, smpls): # compute residuals u_hat = y - l_hat v_hat = d - m_hat @@ -258,7 +258,7 @@ def _score_elements(self, y, d, l_hat, g_hat, m_hat, smpls): psi_b = np.multiply(v_hat, u_hat) else: assert callable(self.score) - psi_a, psi_b = self.score(y, d, l_hat, g_hat, m_hat, smpls) + psi_a, psi_b = self.score(y, d, l_hat, m_hat, g_hat, smpls) return psi_a, psi_b From 27980cf7d6de71419759f0f640f245274587271d Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 11:57:53 +0200 Subject: [PATCH 15/47] add unit test with callable for IV-type score --- doubleml/tests/test_doubleml_scores.py | 44 ++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/doubleml/tests/test_doubleml_scores.py b/doubleml/tests/test_doubleml_scores.py index bfccce58..8bfb15f2 100644 --- a/doubleml/tests/test_doubleml_scores.py +++ b/doubleml/tests/test_doubleml_scores.py @@ -14,6 +14,8 @@ dml_plr = DoubleMLPLR(dml_data_plr, Lasso(), Lasso()) dml_plr.fit() +dml_plr_iv_type = DoubleMLPLR(dml_data_plr, Lasso(), Lasso(), score='IV-type') +dml_plr_iv_type.fit() dml_pliv = DoubleMLPLIV(dml_data_pliv, Lasso(), Lasso(), Lasso()) dml_pliv.fit() dml_irm = DoubleMLIRM(dml_data_irm, Lasso(), LogisticRegression()) @@ -28,6 +30,12 @@ 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.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) @@ -43,7 +51,7 @@ @pytest.mark.ci @pytest.mark.parametrize('dml_obj', - [dml_plr, dml_pliv, dml_irm, dml_iivm]) + [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_a * dml_obj.coef + dml_obj.psi_b, @@ -65,12 +73,9 @@ 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() - if 'ml_g' in preds: - g_hat = preds['ml_g'].squeeze() - else: - g_hat = None + g_hat = None psi_a, psi_b = plr_score(dml_data_plr.y, dml_data_plr.d, - l_hat, g_hat, m_hat, + l_hat, m_hat, g_hat, dml_plr_callable_score.smpls[0]) assert np.allclose(dml_plr.psi_a.squeeze(), psi_a, @@ -80,6 +85,33 @@ def test_plr_callable_vs_pred_export(): 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) + + +@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_a.squeeze(), + psi_a, + rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_iv_type.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, From 16a90d4eafec897dab680e4da67e2628169d2a55 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 29 Apr 2022 14:02:16 +0200 Subject: [PATCH 16/47] increase number of observations in dummy test cases --- doubleml/tests/test_doubleml_exceptions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 6772458f..ba58bc1a 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -10,7 +10,7 @@ from sklearn.base import BaseEstimator np.random.seed(3141) -dml_data = make_plr_CCDDHNR2018(n_obs=10) +dml_data = make_plr_CCDDHNR2018(n_obs=50) ml_l = Lasso() ml_m = Lasso() ml_g = Lasso() @@ -18,11 +18,11 @@ 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_data_irm = make_irm_data(n_obs=10) -dml_data_iivm = make_iivm_data(n_obs=10) -dml_data_pliv = make_pliv_CHS2015(n_obs=10, dim_z=1) +dml_data_irm = make_irm_data(n_obs=50) +dml_data_iivm = make_iivm_data(n_obs=50) +dml_data_pliv = make_pliv_CHS2015(n_obs=50, dim_z=1) dml_cluster_data_pliv = make_pliv_multiway_cluster_CKMS2021(N=10, M=10) -(x, y, d, z) = make_iivm_data(n_obs=30, return_type="array") +(x, y, d, z) = make_iivm_data(n_obs=50, return_type="array") y[y > 0] = 1 y[y < 0] = 0 dml_data_irm_binary_outcome = DoubleMLData.from_arrays(x, y, d) From a7b69721676142190aa396a10b48674ae90a1a17 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 14:59:52 +0200 Subject: [PATCH 17/47] added some additional unit tests for non-orth scores via callable scores --- doubleml/tests/test_doubleml_scores.py | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/doubleml/tests/test_doubleml_scores.py b/doubleml/tests/test_doubleml_scores.py index 8bfb15f2..36e4194b 100644 --- a/doubleml/tests/test_doubleml_scores.py +++ b/doubleml/tests/test_doubleml_scores.py @@ -49,6 +49,34 @@ dml_iivm_callable_score.fit(store_predictions=True) +def non_orth_score_w_g(y, d, l_hat, m_hat, g_hat, smpls): + u_hat = y - g_hat + psi_a = -np.multiply(d, d) + psi_b = np.multiply(d, u_hat) + return psi_a, psi_b + + +def non_orth_score_w_l(y, d, l_hat, m_hat, g_hat, smpls): + p_a = -np.multiply(d - m_hat, d - m_hat) + p_b = np.multiply(d - m_hat, y - l_hat) + theta_initial = -np.nanmean(p_b) / np.nanmean(p_a) + g_hat = l_hat - np.multiply(d, m_hat) + + u_hat = y - g_hat + psi_a = -np.multiply(d, d) + psi_b = np.multiply(d, u_hat) + 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.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.fit(store_predictions=True) + + @pytest.mark.ci @pytest.mark.parametrize('dml_obj', [dml_plr, dml_plr_iv_type, dml_pliv, dml_irm, dml_iivm]) @@ -112,6 +140,40 @@ def test_plr_iv_type_callable_vs_pred_export(): 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_a.squeeze(), + psi_a, + rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_non_orth_score_w_g.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() + 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_a.squeeze(), + psi_a, + rtol=1e-9, atol=1e-4) + assert np.allclose(dml_plr_non_orth_score_w_l.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, From 7d0e0b7b8b7f3e1bcc7efa9a2a34b80dc6d12a8d Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 15:28:51 +0200 Subject: [PATCH 18/47] renamed ml_g into ml_l --- doubleml/double_ml_pliv.py | 82 +++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 77a76678..926e62a1 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -17,9 +17,9 @@ class DoubleMLPLIV(DoubleML): obj_dml_data : :class:`DoubleMLData` object The :class:`DoubleMLData` object providing the data and specifying the variables for the causal model. - ml_g : estimator implementing ``fit()`` and ``predict()`` + ml_l : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`g_0(X) = E[Y|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`l_0(X) = E[Y|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. @@ -63,12 +63,12 @@ class DoubleMLPLIV(DoubleML): >>> from sklearn.base import clone >>> np.random.seed(3141) >>> learner = RandomForestRegressor(n_estimators=100, max_features=20, max_depth=5, min_samples_leaf=2) - >>> ml_g = clone(learner) + >>> ml_l = clone(learner) >>> ml_m = clone(learner) >>> ml_r = clone(learner) >>> data = make_pliv_CHS2015(alpha=0.5, n_obs=500, dim_x=20, dim_z=1, return_type='DataFrame') >>> obj_dml_data = dml.DoubleMLData(data, 'y', 'd', z_cols='Z1') - >>> dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, ml_g, ml_m, ml_r) + >>> dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, ml_l, ml_m, ml_r) >>> dml_pliv_obj.fit().summary coef std err t P>|t| 2.5 % 97.5 % d 0.522753 0.082263 6.354688 2.088504e-10 0.361521 0.683984 @@ -90,7 +90,7 @@ class DoubleMLPLIV(DoubleML): """ def __init__(self, obj_dml_data, - ml_g, + ml_l, ml_m, ml_r, n_folds=5, @@ -108,20 +108,20 @@ def __init__(self, apply_cross_fitting) self._check_data(self._dml_data) - self._check_score(self.score) self.partialX = True self.partialZ = False - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=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_g': ml_g, 'ml_m': ml_m, 'ml_r': ml_r} - self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} + self._learner = {'ml_l': ml_l, 'ml_m': ml_m, 'ml_r': ml_r} + self._predict_method = {'ml_l': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} self._initialize_ml_nuisance_params() @classmethod def _partialX(cls, obj_dml_data, - ml_g, + ml_l, ml_m, ml_r, n_folds=5, @@ -131,7 +131,7 @@ def _partialX(cls, draw_sample_splitting=True, apply_cross_fitting=True): obj = cls(obj_dml_data, - ml_g, + ml_l, ml_m, ml_r, n_folds, @@ -141,14 +141,14 @@ def _partialX(cls, draw_sample_splitting, apply_cross_fitting) obj._check_data(obj._dml_data) - obj._check_score(obj.score) obj.partialX = True obj.partialZ = False - _ = obj._check_learner(ml_g, 'ml_g', regressor=True, classifier=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_g': ml_g, 'ml_m': ml_m, 'ml_r': ml_r} - obj._predict_method = {'ml_g': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} + 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 @@ -162,7 +162,7 @@ def _partialZ(cls, dml_procedure='dml2', draw_sample_splitting=True, apply_cross_fitting=True): - # to pass the checks for the learners, we temporarily set ml_g and ml_m to DummyRegressor() + # to pass the checks for the learners, we temporarily set ml_l and ml_m to DummyRegressor() obj = cls(obj_dml_data, DummyRegressor(), DummyRegressor(), @@ -174,9 +174,9 @@ def _partialZ(cls, draw_sample_splitting, apply_cross_fitting) obj._check_data(obj._dml_data) - obj._check_score(obj.score) 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'} @@ -186,7 +186,7 @@ def _partialZ(cls, @classmethod def _partialXZ(cls, obj_dml_data, - ml_g, + ml_l, ml_m, ml_r, n_folds=5, @@ -196,7 +196,7 @@ def _partialXZ(cls, draw_sample_splitting=True, apply_cross_fitting=True): obj = cls(obj_dml_data, - ml_g, + ml_l, ml_m, ml_r, n_folds, @@ -206,28 +206,28 @@ def _partialXZ(cls, draw_sample_splitting, apply_cross_fitting) obj._check_data(obj._dml_data) - obj._check_score(obj.score) obj.partialX = True obj.partialZ = True - _ = obj._check_learner(ml_g, 'ml_g', regressor=True, classifier=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_g': ml_g, 'ml_m': ml_m, 'ml_r': ml_r} - obj._predict_method = {'ml_g': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} + 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): if self._dml_data.n_instr == 1: - valid_learner = ['ml_g', 'ml_m', 'ml_r'] + valid_learner = ['ml_l', 'ml_m', 'ml_r'] else: - valid_learner = ['ml_g', 'ml_r'] + ['ml_m_' + z_col for z_col in self._dml_data.z_cols] + valid_learner = ['ml_l', 'ml_r'] + ['ml_m_' + z_col for z_col in self._dml_data.z_cols] elif (not self.partialX) & self.partialZ: valid_learner = ['ml_r'] else: assert (self.partialX & self.partialZ) - valid_learner = ['ml_g', 'ml_m', 'ml_r'] + valid_learner = ['ml_l', 'ml_m', 'ml_r'] self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} for learner in valid_learner} @@ -288,9 +288,9 @@ def _nuisance_est_partial_x(self, smpls, n_jobs_cv): force_all_finite=False) # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_g'], x, y, 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, self._learner['ml_g'], 'ml_g', smpls) + g_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(g_hat, self._learner['ml_l'], 'ml_l', smpls) # nuisance m if self._dml_data.n_instr == 1: @@ -317,7 +317,7 @@ def _nuisance_est_partial_x(self, smpls, n_jobs_cv): _check_finite_predictions(r_hat, self._learner['ml_r'], 'ml_r', smpls) psi_a, psi_b = self._score_elements(y, z, d, g_hat, m_hat, r_hat, smpls) - preds = {'ml_g': g_hat, + preds = {'ml_l': g_hat, 'ml_m': m_hat, 'ml_r': r_hat} @@ -390,9 +390,9 @@ def _nuisance_est_partial_xz(self, smpls, n_jobs_cv): force_all_finite=False) # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_g'], x, y, 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, self._learner['ml_g'], 'ml_g', smpls) + g_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(g_hat, self._learner['ml_l'], 'ml_l', smpls) # nuisance m m_hat, m_hat_on_train = _dml_cv_predict(self._learner['ml_m'], xz, d, smpls=smpls, n_jobs=n_jobs_cv, @@ -417,7 +417,7 @@ def _nuisance_est_partial_xz(self, smpls, n_jobs_cv): assert callable(self.score) raise NotImplementedError('Callable score not implemented for DoubleMLPLIV.partialXZ.') - preds = {'ml_g': g_hat, + preds = {'ml_l': g_hat, 'ml_m': m_hat, 'ml_r': m_hat_tilde} @@ -431,13 +431,13 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_g': None, + scoring_methods = {'ml_l': None, 'ml_m': None, 'ml_r': None} train_inds = [train_index for (train_index, _) in smpls] g_tune_res = _dml_tune(y, x, train_inds, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], + 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: @@ -467,13 +467,13 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold g_best_params = [xx.best_params_ for xx in g_tune_res] r_best_params = [xx.best_params_ for xx in r_tune_res] if self._dml_data.n_instr > 1: - params = {'ml_g': g_best_params, + params = {'ml_l': g_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]] else: m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_g': g_best_params, + params = {'ml_l': g_best_params, 'ml_m': m_best_params, 'ml_r': r_best_params} @@ -522,13 +522,13 @@ def _nuisance_tuning_partial_xz(self, smpls, param_grids, scoring_methods, n_fol force_all_finite=False) if scoring_methods is None: - scoring_methods = {'ml_g': None, + scoring_methods = {'ml_l': None, 'ml_m': None, 'ml_r': None} train_inds = [train_index for (train_index, _) in smpls] g_tune_res = _dml_tune(y, x, train_inds, - self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'], + 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'], @@ -554,7 +554,7 @@ def _nuisance_tuning_partial_xz(self, smpls, param_grids, scoring_methods, n_fol 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_g': g_best_params, + params = {'ml_l': g_best_params, 'ml_m': m_best_params, 'ml_r': r_best_params} From 871b7628c6624e1258759808c81e3d6e2220b1dd Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 15:31:05 +0200 Subject: [PATCH 19/47] also rename g_hat into l_hat; start implementing the IV-type score --- doubleml/double_ml_pliv.py | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 926e62a1..d8696574 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -39,7 +39,7 @@ class DoubleMLPLIV(DoubleML): score : str or callable A str (``'partialling out'`` is the only choice) specifying the score function - or a callable object / function with signature ``psi_a, psi_b = score(y, z, d, g_hat, m_hat, r_hat, smpls)``. + or a callable object / function with signature ``psi_a, psi_b = score(y, z, d, l_hat, m_hat, r_hat, smpls)``. Default is ``'partialling out'``. dml_procedure : str @@ -233,14 +233,13 @@ def _initialize_ml_nuisance_params(self): def _check_score(self, score): if isinstance(score, str): - valid_score = ['partialling out'] - # check whether its worth implementing the IV_type as well - # In CCDHNR equation (4.7) a score of this type is provided; - # however in the following paragraph it is explained that one might - # still need to estimate the partialling out type first + if self.partialX & (not self.partialZ) & (self._dml_data.n_instr == 1): + valid_score = ['partialling out', 'IV-type'] + else: + valid_score = ['partialling out'] if score not in valid_score: raise ValueError('Invalid score ' + score + '. ' + - 'Valid score ' + 'partialling out.') + 'Valid score ' + ' or '.join(valid_score) + '.') else: if not callable(score): raise TypeError('score should be either a string or a callable. ' @@ -287,10 +286,10 @@ def _nuisance_est_partial_x(self, smpls, n_jobs_cv): x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) - # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv, + # 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(g_hat, self._learner['ml_l'], 'ml_l', smpls) + _check_finite_predictions(l_hat, self._learner['ml_l'], 'ml_l', smpls) # nuisance m if self._dml_data.n_instr == 1: @@ -316,16 +315,16 @@ def _nuisance_est_partial_x(self, smpls, n_jobs_cv): est_params=self._get_params('ml_r'), method=self._predict_method['ml_r']) _check_finite_predictions(r_hat, self._learner['ml_r'], 'ml_r', smpls) - psi_a, psi_b = self._score_elements(y, z, d, g_hat, m_hat, r_hat, smpls) - preds = {'ml_l': g_hat, + psi_a, psi_b = self._score_elements(y, z, d, l_hat, m_hat, r_hat, smpls) + preds = {'ml_l': l_hat, 'ml_m': m_hat, 'ml_r': r_hat} return psi_a, psi_b, preds - def _score_elements(self, y, z, d, g_hat, m_hat, r_hat, smpls): + def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, smpls): # compute residuals - u_hat = y - g_hat + u_hat = y - l_hat w_hat = d - r_hat v_hat = z - m_hat @@ -353,7 +352,7 @@ def _score_elements(self, y, z, d, g_hat, m_hat, r_hat, smpls): else: assert self._dml_data.n_instr == 1 psi_a, psi_b = self.score(y, z, d, - g_hat, m_hat, r_hat, smpls) + l_hat, m_hat, r_hat, smpls) return psi_a, psi_b @@ -389,10 +388,10 @@ def _nuisance_est_partial_xz(self, smpls, n_jobs_cv): x, d = check_X_y(x, self._dml_data.d, force_all_finite=False) - # nuisance g - g_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv, + # 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(g_hat, self._learner['ml_l'], 'ml_l', smpls) + _check_finite_predictions(l_hat, self._learner['ml_l'], 'ml_l', smpls) # nuisance m m_hat, m_hat_on_train = _dml_cv_predict(self._learner['ml_m'], xz, d, smpls=smpls, n_jobs=n_jobs_cv, @@ -406,7 +405,7 @@ def _nuisance_est_partial_xz(self, smpls, n_jobs_cv): _check_finite_predictions(m_hat_tilde, self._learner['ml_r'], 'ml_r', smpls) # compute residuals - u_hat = y - g_hat + u_hat = y - l_hat w_hat = d - m_hat_tilde if isinstance(self.score, str): @@ -417,7 +416,7 @@ def _nuisance_est_partial_xz(self, smpls, n_jobs_cv): assert callable(self.score) raise NotImplementedError('Callable score not implemented for DoubleMLPLIV.partialXZ.') - preds = {'ml_l': g_hat, + preds = {'ml_l': l_hat, 'ml_m': m_hat, 'ml_r': m_hat_tilde} From b93e0e81f957837ce81983028d5572c190076381 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 15:39:48 +0200 Subject: [PATCH 20/47] renaming of ml_g to ml_l also in the unit tests --- doubleml/tests/_utils_pliv_manual.py | 54 +++++++++--------- .../tests/_utils_pliv_partial_x_manual.py | 56 +++++++++---------- .../tests/_utils_pliv_partial_xz_manual.py | 56 +++++++++---------- doubleml/tests/test_pliv.py | 6 +- 4 files changed, 86 insertions(+), 86 deletions(-) diff --git a/doubleml/tests/_utils_pliv_manual.py b/doubleml/tests/_utils_pliv_manual.py index 75e1d707..679d2ddb 100644 --- a/doubleml/tests/_utils_pliv_manual.py +++ b/doubleml/tests/_utils_pliv_manual.py @@ -5,37 +5,37 @@ def fit_pliv(y, x, d, z, - learner_g, learner_m, learner_r, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, m_params=None, r_params=None): + learner_l, learner_m, learner_r, all_smpls, dml_procedure, score, + n_rep=1, l_params=None, m_params=None, r_params=None): n_obs = len(y) thetas = np.zeros(n_rep) ses = np.zeros(n_rep) - all_g_hat = list() + all_l_hat = list() all_m_hat = list() all_r_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat, r_hat = fit_nuisance_pliv(y, x, d, z, - learner_g, learner_m, learner_r, + l_hat, m_hat, r_hat = fit_nuisance_pliv(y, x, d, z, + learner_l, learner_m, learner_r, smpls, - g_params, m_params, r_params) + l_params, m_params, r_params) - all_g_hat.append(g_hat) + all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_r_hat.append(r_hat) if dml_procedure == 'dml1': thetas[i_rep], ses[i_rep] = pliv_dml1(y, x, d, z, - g_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, smpls, score) else: assert dml_procedure == 'dml2' thetas[i_rep], ses[i_rep] = pliv_dml2(y, x, d, z, - g_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, smpls, score) theta = np.median(thetas) @@ -43,51 +43,51 @@ def fit_pliv(y, x, d, z, res = {'theta': theta, 'se': se, 'thetas': thetas, 'ses': ses, - 'all_g_hat': all_g_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} + 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} return res -def fit_nuisance_pliv(y, x, d, z, ml_g, ml_m, ml_r, smpls, g_params=None, m_params=None, r_params=None): - g_hat = fit_predict(y, x, ml_g, g_params, smpls) +def fit_nuisance_pliv(y, x, d, z, ml_l, ml_m, ml_r, smpls, l_params=None, m_params=None, r_params=None): + l_hat = fit_predict(y, x, ml_l, l_params, smpls) m_hat = fit_predict(z, x, ml_m, m_params, smpls) r_hat = fit_predict(d, x, ml_r, r_params, smpls) - return g_hat, m_hat, r_hat + return l_hat, m_hat, r_hat -def tune_nuisance_pliv(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_tune, param_grid_g, param_grid_m, param_grid_r): - g_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune) +def tune_nuisance_pliv(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 = tune_grid_search(z, x, ml_m, smpls, param_grid_m, n_folds_tune) r_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune) - g_best_params = [xx.best_params_ for xx in g_tune_res] + 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] - return g_best_params, m_best_params, r_best_params + return l_best_params, m_best_params, r_best_params -def compute_pliv_residuals(y, d, z, g_hat, m_hat, r_hat, smpls): +def compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls): u_hat = np.full_like(y, np.nan, dtype='float64') v_hat = np.full_like(z, np.nan, dtype='float64') w_hat = np.full_like(d, np.nan, dtype='float64') for idx, (_, test_index) in enumerate(smpls): - u_hat[test_index] = y[test_index] - g_hat[idx] + u_hat[test_index] = y[test_index] - l_hat[idx] v_hat[test_index] = z[test_index] - m_hat[idx] w_hat[test_index] = d[test_index] - r_hat[idx] return u_hat, v_hat, w_hat -def pliv_dml1(y, x, d, z, g_hat, m_hat, r_hat, smpls, score): +def pliv_dml1(y, x, d, z, l_hat, m_hat, r_hat, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, g_hat, m_hat, r_hat, smpls) + u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls) for idx, (_, test_index) in enumerate(smpls): thetas[idx] = pliv_orth(u_hat[test_index], v_hat[test_index], w_hat[test_index], d[test_index], score) @@ -106,9 +106,9 @@ def pliv_dml1(y, x, d, z, g_hat, m_hat, r_hat, smpls, score): return theta_hat, se -def pliv_dml2(y, x, d, z, g_hat, m_hat, r_hat, smpls, score): +def pliv_dml2(y, x, d, z, l_hat, m_hat, r_hat, smpls, score): n_obs = len(y) - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, g_hat, m_hat, r_hat, smpls) + u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls) theta_hat = pliv_orth(u_hat, v_hat, w_hat, d, score) se = np.sqrt(var_pliv(theta_hat, d, u_hat, v_hat, w_hat, score, n_obs)) @@ -130,7 +130,7 @@ def pliv_orth(u_hat, v_hat, w_hat, d, score): return res -def boot_pliv(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, +def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, all_smpls, score, bootstrap, n_rep_boot, n_rep=1, apply_cross_fitting=True): all_boot_theta = list() @@ -144,7 +144,7 @@ def boot_pliv(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, n_obs = len(test_index) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_theta, boot_t_stat = boot_pliv_single_split( - thetas[i_rep], y, d, z, all_g_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], smpls, + thetas[i_rep], y, d, z, all_l_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], smpls, score, ses[i_rep], weights, n_rep_boot, apply_cross_fitting) all_boot_theta.append(boot_theta) all_boot_t_stat.append(boot_t_stat) @@ -155,10 +155,10 @@ def boot_pliv(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, return boot_theta, boot_t_stat -def boot_pliv_single_split(theta, y, d, z, g_hat, m_hat, r_hat, +def boot_pliv_single_split(theta, y, d, z, l_hat, m_hat, r_hat, smpls, score, se, weights, n_rep_boot, apply_cross_fitting): assert score == 'partialling out' - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, g_hat, m_hat, r_hat, smpls) + u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls) if apply_cross_fitting: J = np.mean(-np.multiply(v_hat, w_hat)) diff --git a/doubleml/tests/_utils_pliv_partial_x_manual.py b/doubleml/tests/_utils_pliv_partial_x_manual.py index 53716e01..e83d7e83 100644 --- a/doubleml/tests/_utils_pliv_partial_x_manual.py +++ b/doubleml/tests/_utils_pliv_partial_x_manual.py @@ -6,37 +6,37 @@ def fit_pliv_partial_x(y, x, d, z, - learner_g, learner_m, learner_r, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, m_params=None, r_params=None): + learner_l, learner_m, learner_r, all_smpls, dml_procedure, score, + n_rep=1, l_params=None, m_params=None, r_params=None): n_obs = len(y) thetas = np.zeros(n_rep) ses = np.zeros(n_rep) - all_g_hat = list() + all_l_hat = list() all_m_hat = list() all_r_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat, r_hat = fit_nuisance_pliv_partial_x(y, x, d, z, - learner_g, learner_m, learner_r, + l_hat, m_hat, r_hat = fit_nuisance_pliv_partial_x(y, x, d, z, + learner_l, learner_m, learner_r, smpls, - g_params, m_params, r_params) + l_params, m_params, r_params) - all_g_hat.append(g_hat) + all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_r_hat.append(r_hat) if dml_procedure == 'dml1': thetas[i_rep], ses[i_rep] = pliv_partial_x_dml1(y, x, d, z, - g_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, smpls, score) else: assert dml_procedure == 'dml2' thetas[i_rep], ses[i_rep] = pliv_partial_x_dml2(y, x, d, z, - g_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, smpls, score) theta = np.median(thetas) @@ -44,14 +44,14 @@ def fit_pliv_partial_x(y, x, d, z, res = {'theta': theta, 'se': se, 'thetas': thetas, 'ses': ses, - 'all_g_hat': all_g_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} + 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} return res -def fit_nuisance_pliv_partial_x(y, x, d, z, ml_g, ml_m, ml_r, smpls, g_params=None, m_params=None, r_params=None): +def fit_nuisance_pliv_partial_x(y, x, d, z, ml_l, ml_m, ml_r, smpls, l_params=None, m_params=None, r_params=None): assert z.ndim == 2 - g_hat = fit_predict(y, x, ml_g, g_params, smpls) + l_hat = fit_predict(y, x, ml_l, l_params, smpls) m_hat = list() for i_instr in range(z.shape[1]): @@ -71,12 +71,12 @@ def fit_nuisance_pliv_partial_x(y, x, d, z, ml_g, ml_m, ml_r, smpls, g_params=No r_hat_tilde = LinearRegression(fit_intercept=True).fit(z - m_hat_array, d - r_hat_array).predict(z - m_hat_array) - return g_hat, r_hat, r_hat_tilde + return l_hat, r_hat, r_hat_tilde -def tune_nuisance_pliv_partial_x(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_tune, - param_grid_g, param_grid_m, param_grid_r): - g_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune) +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() for i_instr in range(z.shape[1]): @@ -84,27 +84,27 @@ def tune_nuisance_pliv_partial_x(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_tu r_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune) - g_best_params = [xx.best_params_ for xx in g_tune_res] + l_best_params = [xx.best_params_ for xx in l_tune_res] m_best_params = [[xx.best_params_ for xx in m_tune_res[i_instr]] for i_instr in range(z.shape[1])] r_best_params = [xx.best_params_ for xx in r_tune_res] - return g_best_params, m_best_params, r_best_params + return l_best_params, m_best_params, r_best_params -def compute_pliv_partial_x_residuals(y, d, g_hat, r_hat, smpls): +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') for idx, (_, test_index) in enumerate(smpls): - u_hat[test_index] = y[test_index] - g_hat[idx] + u_hat[test_index] = y[test_index] - l_hat[idx] w_hat[test_index] = d[test_index] - r_hat[idx] return u_hat, w_hat -def pliv_partial_x_dml1(y, x, d, z, g_hat, r_hat, r_hat_tilde, smpls, score): +def pliv_partial_x_dml1(y, x, d, z, l_hat, r_hat, r_hat_tilde, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - u_hat, w_hat = compute_pliv_partial_x_residuals(y, d, g_hat, r_hat, smpls) + u_hat, w_hat = compute_pliv_partial_x_residuals(y, d, l_hat, r_hat, smpls) for idx, (_, test_index) in enumerate(smpls): thetas[idx] = pliv_partial_x_orth(u_hat[test_index], w_hat[test_index], r_hat_tilde[test_index], @@ -116,9 +116,9 @@ def pliv_partial_x_dml1(y, x, d, z, g_hat, r_hat, r_hat_tilde, smpls, score): return theta_hat, se -def pliv_partial_x_dml2(y, x, d, z, g_hat, r_hat, r_hat_tilde, smpls, score): +def pliv_partial_x_dml2(y, x, d, z, l_hat, r_hat, r_hat_tilde, smpls, score): n_obs = len(y) - u_hat, w_hat = compute_pliv_partial_x_residuals(y, d, g_hat, r_hat, smpls) + u_hat, w_hat = compute_pliv_partial_x_residuals(y, d, l_hat, r_hat, smpls) theta_hat = pliv_partial_x_orth(u_hat, w_hat, r_hat_tilde, d, score) se = np.sqrt(var_pliv_partial_x(theta_hat, d, u_hat, w_hat, r_hat_tilde, score, n_obs)) @@ -140,7 +140,7 @@ def pliv_partial_x_orth(u_hat, w_hat, r_hat_tilde, d, score): return res -def boot_pliv_partial_x(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, +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_theta = list() @@ -149,7 +149,7 @@ def boot_pliv_partial_x(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, n_obs = len(y) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_theta, boot_t_stat = boot_pliv_partial_x_single_split( - thetas[i_rep], y, d, z, all_g_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], all_smpls[i_rep], + 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_theta.append(boot_theta) all_boot_t_stat.append(boot_t_stat) @@ -160,10 +160,10 @@ def boot_pliv_partial_x(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, return boot_theta, boot_t_stat -def boot_pliv_partial_x_single_split(theta, y, d, z, g_hat, r_hat, r_hat_tilde, +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, g_hat, r_hat, smpls) + 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)) diff --git a/doubleml/tests/_utils_pliv_partial_xz_manual.py b/doubleml/tests/_utils_pliv_partial_xz_manual.py index 515d373b..ba28ee06 100644 --- a/doubleml/tests/_utils_pliv_partial_xz_manual.py +++ b/doubleml/tests/_utils_pliv_partial_xz_manual.py @@ -6,37 +6,37 @@ def fit_pliv_partial_xz(y, x, d, z, - learner_g, learner_m, learner_r, all_smpls, dml_procedure, score, - n_rep=1, g_params=None, m_params=None, r_params=None): + learner_l, learner_m, learner_r, all_smpls, dml_procedure, score, + n_rep=1, l_params=None, m_params=None, r_params=None): n_obs = len(y) thetas = np.zeros(n_rep) ses = np.zeros(n_rep) - all_g_hat = list() + all_l_hat = list() all_m_hat = list() all_r_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - g_hat, m_hat, r_hat = fit_nuisance_pliv_partial_xz(y, x, d, z, - learner_g, learner_m, learner_r, + l_hat, m_hat, r_hat = fit_nuisance_pliv_partial_xz(y, x, d, z, + learner_l, learner_m, learner_r, smpls, - g_params, m_params, r_params) + l_params, m_params, r_params) - all_g_hat.append(g_hat) + all_l_hat.append(l_hat) all_m_hat.append(m_hat) all_r_hat.append(r_hat) if dml_procedure == 'dml1': thetas[i_rep], ses[i_rep] = pliv_partial_xz_dml1(y, x, d, z, - g_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, smpls, score) else: assert dml_procedure == 'dml2' thetas[i_rep], ses[i_rep] = pliv_partial_xz_dml2(y, x, d, z, - g_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, smpls, score) theta = np.median(thetas) @@ -44,13 +44,13 @@ def fit_pliv_partial_xz(y, x, d, z, res = {'theta': theta, 'se': se, 'thetas': thetas, 'ses': ses, - 'all_g_hat': all_g_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} + 'all_l_hat': all_l_hat, 'all_m_hat': all_m_hat, 'all_r_hat': all_r_hat} return res -def fit_nuisance_pliv_partial_xz(y, x, d, z, ml_g, ml_m, ml_r, smpls, g_params=None, m_params=None, r_params=None): - g_hat = fit_predict(y, x, ml_g, g_params, smpls) +def fit_nuisance_pliv_partial_xz(y, x, d, z, ml_l, ml_m, ml_r, smpls, l_params=None, m_params=None, r_params=None): + l_hat = fit_predict(y, x, ml_l, l_params, smpls) xz = np.hstack((x, z)) m_hat = [] @@ -68,12 +68,12 @@ def fit_nuisance_pliv_partial_xz(y, x, d, z, ml_g, ml_m, ml_r, smpls, g_params=N ml_r.set_params(**r_params[idx]) m_hat_tilde.append(ml_r.fit(x[train_index], m_hat_train[idx]).predict(x[test_index])) - return g_hat, m_hat, m_hat_tilde + return l_hat, m_hat, m_hat_tilde -def tune_nuisance_pliv_partial_xz(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_tune, - param_grid_g, param_grid_m, param_grid_r): - g_tune_res = tune_grid_search(y, x, ml_g, smpls, param_grid_g, n_folds_tune) +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)) m_tune_res = tune_grid_search(d, xz, ml_m, smpls, param_grid_m, n_folds_tune) @@ -86,29 +86,29 @@ def tune_nuisance_pliv_partial_xz(y, x, d, z, ml_g, ml_m, ml_r, smpls, n_folds_t cv=r_tune_resampling) r_tune_res[idx] = r_grid_search.fit(x[train_index, :], m_hat) - g_best_params = [xx.best_params_ for xx in g_tune_res] + 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] - return g_best_params, m_best_params, r_best_params + return l_best_params, m_best_params, r_best_params -def compute_pliv_partial_xz_residuals(y, d, g_hat, m_hat, m_hat_tilde, smpls): +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') for idx, (_, test_index) in enumerate(smpls): - u_hat[test_index] = y[test_index] - g_hat[idx] + u_hat[test_index] = y[test_index] - l_hat[idx] v_hat[test_index] = m_hat[idx] - m_hat_tilde[idx] w_hat[test_index] = d[test_index] - m_hat_tilde[idx] return u_hat, v_hat, w_hat -def pliv_partial_xz_dml1(y, x, d, z, g_hat, m_hat, m_hat_tilde, smpls, score): +def pliv_partial_xz_dml1(y, x, d, z, l_hat, m_hat, m_hat_tilde, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - u_hat, v_hat, w_hat = compute_pliv_partial_xz_residuals(y, d, g_hat, m_hat, m_hat_tilde, smpls) + u_hat, v_hat, w_hat = compute_pliv_partial_xz_residuals(y, d, l_hat, m_hat, m_hat_tilde, smpls) for idx, (_, test_index) in enumerate(smpls): thetas[idx] = pliv_partial_xz_orth(u_hat[test_index], v_hat[test_index], w_hat[test_index], @@ -120,9 +120,9 @@ def pliv_partial_xz_dml1(y, x, d, z, g_hat, m_hat, m_hat_tilde, smpls, score): return theta_hat, se -def pliv_partial_xz_dml2(y, x, d, z, g_hat, m_hat, m_hat_tilde, smpls, score): +def pliv_partial_xz_dml2(y, x, d, z, l_hat, m_hat, m_hat_tilde, smpls, score): n_obs = len(y) - u_hat, v_hat, w_hat = compute_pliv_partial_xz_residuals(y, d, g_hat, m_hat, m_hat_tilde, smpls) + u_hat, v_hat, w_hat = compute_pliv_partial_xz_residuals(y, d, l_hat, m_hat, m_hat_tilde, smpls) theta_hat = pliv_partial_xz_orth(u_hat, v_hat, w_hat, d, score) se = np.sqrt(var_pliv_partial_xz(theta_hat, d, u_hat, v_hat, w_hat, score, n_obs)) @@ -144,7 +144,7 @@ def pliv_partial_xz_orth(u_hat, v_hat, w_hat, d, score): return res -def boot_pliv_partial_xz(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, +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_theta = list() @@ -153,7 +153,7 @@ def boot_pliv_partial_xz(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, n_obs = len(y) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_theta, boot_t_stat = boot_pliv_partial_xz_single_split( - thetas[i_rep], y, d, z, all_g_hat[i_rep], all_m_hat[i_rep], all_r_hat[i_rep], all_smpls[i_rep], + 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_theta.append(boot_theta) all_boot_t_stat.append(boot_t_stat) @@ -164,10 +164,10 @@ def boot_pliv_partial_xz(y, d, z, thetas, ses, all_g_hat, all_m_hat, all_r_hat, return boot_theta, boot_t_stat -def boot_pliv_partial_xz_single_split(theta, y, d, z, g_hat, m_hat, m_hat_tilde, +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, g_hat, m_hat, m_hat_tilde, smpls) + 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)) diff --git a/doubleml/tests/test_pliv.py b/doubleml/tests/test_pliv.py index a2f9a4ac..8a216bea 100644 --- a/doubleml/tests/test_pliv.py +++ b/doubleml/tests/test_pliv.py @@ -43,15 +43,15 @@ def dml_pliv_fixture(generate_data_iv, learner, score, dml_procedure): data = generate_data_iv x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'Z1') dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_g, ml_m, ml_r, + ml_l, ml_m, ml_r, n_folds, dml_procedure=dml_procedure) From f504590e59694baf6816d29bd89ca5ba386eb13a Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 15:43:22 +0200 Subject: [PATCH 21/47] also align names in the tuning parts with the renamed learner ml_g to ml_l --- doubleml/double_ml_pliv.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index d8696574..6af5774f 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -435,7 +435,7 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold 'ml_r': None} train_inds = [train_index for (train_index, _) in smpls] - g_tune_res = _dml_tune(y, x, train_inds, + 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) @@ -463,20 +463,20 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold self._learner['ml_r'], param_grids['ml_r'], scoring_methods['ml_r'], n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search) - g_best_params = [xx.best_params_ for xx in g_tune_res] + 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': g_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]] else: m_best_params = [xx.best_params_ for xx in m_tune_res] - params = {'ml_l': g_best_params, + params = {'ml_l': l_best_params, 'ml_m': m_best_params, 'ml_r': r_best_params} - tune_res = {'g_tune': g_tune_res, + tune_res = {'l_tune': l_tune_res, 'm_tune': m_tune_res, 'r_tune': r_tune_res} @@ -526,7 +526,7 @@ def _nuisance_tuning_partial_xz(self, smpls, param_grids, scoring_methods, n_fol 'ml_r': None} train_inds = [train_index for (train_index, _) in smpls] - g_tune_res = _dml_tune(y, x, train_inds, + 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, @@ -549,15 +549,15 @@ def _nuisance_tuning_partial_xz(self, smpls, param_grids, scoring_methods, n_fol n_iter=n_iter_randomized_search) r_tune_res.append(r_grid_search.fit(x[train_index, :], m_hat)) - g_best_params = [xx.best_params_ for xx in g_tune_res] + 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': g_best_params, + params = {'ml_l': l_best_params, 'ml_m': m_best_params, 'ml_r': r_best_params} - tune_res = {'g_tune': g_tune_res, + tune_res = {'l_tune': l_tune_res, 'm_tune': m_tune_res, 'r_tune': r_tune_res} From 22a2a72ee9f555114724de627c7f8d41cc3958b4 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 17:35:56 +0200 Subject: [PATCH 22/47] adapt unit test after renaming ml_g to ml_l --- doubleml/tests/test_multiway_cluster.py | 48 ++++++++++----------- doubleml/tests/test_pliv.py | 4 +- doubleml/tests/test_pliv_no_cross_fit.py | 10 ++--- doubleml/tests/test_pliv_partial_x.py | 10 ++--- doubleml/tests/test_pliv_partial_x_tune.py | 34 +++++++-------- doubleml/tests/test_pliv_partial_xz.py | 10 ++--- doubleml/tests/test_pliv_partial_xz_tune.py | 34 +++++++-------- doubleml/tests/test_pliv_partial_z.py | 2 +- doubleml/tests/test_pliv_partial_z_tune.py | 2 +- doubleml/tests/test_pliv_tune.py | 34 +++++++-------- 10 files changed, 94 insertions(+), 94 deletions(-) diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index 9e18f28b..3c3d4782 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -55,8 +55,8 @@ def dml_pliv_multiway_cluster_old_vs_new_fixture(generate_data_iv, learner): obj_dml_multiway_resampling = DoubleMLMultiwayResampling(n_folds, smpl_sizes) _, smpls_lin_ind = obj_dml_multiway_resampling.split_samples() - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) @@ -68,8 +68,8 @@ def dml_pliv_multiway_cluster_old_vs_new_fixture(generate_data_iv, learner): z_cols=obj_dml_cluster_data.z_cols) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure, draw_sample_splitting=False) dml_pliv_obj.set_sample_splitting(smpls_lin_ind) @@ -78,8 +78,8 @@ def dml_pliv_multiway_cluster_old_vs_new_fixture(generate_data_iv, learner): np.random.seed(3141) dml_pliv_obj_cluster = dml.DoubleMLPLIV(obj_dml_cluster_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure) dml_pliv_obj_cluster.fit() @@ -102,15 +102,15 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, dml_procedure): n_rep = 2 score = 'partialling out' - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_cluster_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, n_rep=n_rep, score=score, dml_procedure=dml_procedure) @@ -131,11 +131,11 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, dml_procedure): thetas = np.full(n_rep, np.nan) ses = np.full(n_rep, np.nan) for i_rep in range(n_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] smpls_one_split = dml_pliv_obj.smpls[i_rep] - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, g_hat, m_hat, r_hat, smpls_one_split) + u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls_one_split) psi_a = -np.multiply(v_hat, w_hat) if dml_procedure == 'dml2': @@ -188,15 +188,15 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, dml_procedure): n_folds = 3 score = 'partialling out' - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_oneway_cluster_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, score=score, dml_procedure=dml_procedure) @@ -212,11 +212,11 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, dml_procedure): res_manual = fit_pliv(y, x, d, z, clone(learner), clone(learner), clone(learner), dml_pliv_obj.smpls, dml_procedure, score) - g_hat = res_manual['all_g_hat'][0] + l_hat = res_manual['all_l_hat'][0] m_hat = res_manual['all_m_hat'][0] r_hat = res_manual['all_r_hat'][0] smpls_one_split = dml_pliv_obj.smpls[0] - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, g_hat, m_hat, r_hat, smpls_one_split) + u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls_one_split) psi_a = -np.multiply(v_hat, w_hat) if dml_procedure == 'dml2': @@ -263,15 +263,15 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_g = clone(learner) + # 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) np.random.seed(3141) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, - n_folds, + ml_l, ml_m, + n_folds=n_folds, dml_procedure=dml_procedure) dml_plr_obj.fit() @@ -283,8 +283,8 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): cluster_cols='index') np.random.seed(3141) dml_plr_cluster_obj = dml.DoubleMLPLR(dml_cluster_data, - ml_g, ml_m, - n_folds, + ml_l, ml_m, + n_folds=n_folds, dml_procedure=dml_procedure) dml_plr_cluster_obj.fit() diff --git a/doubleml/tests/test_pliv.py b/doubleml/tests/test_pliv.py index 8a216bea..9e0dbb86 100644 --- a/doubleml/tests/test_pliv.py +++ b/doubleml/tests/test_pliv.py @@ -52,7 +52,7 @@ def dml_pliv_fixture(generate_data_iv, learner, score, dml_procedure): 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, - n_folds, + n_folds=n_folds, dml_procedure=dml_procedure) dml_pliv_obj.fit() @@ -77,7 +77,7 @@ def dml_pliv_fixture(generate_data_iv, learner, score, dml_procedure): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], + 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) diff --git a/doubleml/tests/test_pliv_no_cross_fit.py b/doubleml/tests/test_pliv_no_cross_fit.py index 7265742f..ade7a74b 100644 --- a/doubleml/tests/test_pliv_no_cross_fit.py +++ b/doubleml/tests/test_pliv_no_cross_fit.py @@ -40,16 +40,16 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): data = generate_data_iv x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'Z1') dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure, apply_cross_fitting=False) @@ -80,7 +80,7 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], [smpls], score, bootstrap, n_rep_boot, apply_cross_fitting=False) diff --git a/doubleml/tests/test_pliv_partial_x.py b/doubleml/tests/test_pliv_partial_x.py index 49affaf3..63f5b52f 100644 --- a/doubleml/tests/test_pliv_partial_x.py +++ b/doubleml/tests/test_pliv_partial_x.py @@ -39,15 +39,15 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner, score, dml_ # collect data obj_dml_data = generate_data_pliv_partialX - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV._partialX(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure) dml_pliv_obj.fit() @@ -73,7 +73,7 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner, score, dml_ for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv_partial_x(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], all_smpls, score, bootstrap, n_rep_boot) diff --git a/doubleml/tests/test_pliv_partial_x_tune.py b/doubleml/tests/test_pliv_partial_x_tune.py index 88818ced..a1cedd0e 100644 --- a/doubleml/tests/test_pliv_partial_x_tune.py +++ b/doubleml/tests/test_pliv_partial_x_tune.py @@ -15,7 +15,7 @@ @pytest.fixture(scope='module', params=[ElasticNet()]) -def learner_g(request): +def learner_l(request): return request.param @@ -59,9 +59,9 @@ def get_par_grid(learner): @pytest.fixture(scope='module') -def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_g, learner_m, learner_r, score, +def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_l, learner_m, learner_r, score, dml_procedure, tune_on_folds): - par_grid = {'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)} n_folds_tune = 4 @@ -73,15 +73,15 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_g, learner_m # collect data obj_dml_data = generate_data_pliv_partialX - # Set machine learning methods for g, m & r - ml_g = clone(learner_g) + # Set machine learning methods for l, m & r + ml_l = clone(learner_l) ml_m = clone(learner_m) ml_r = clone(learner_r) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV._partialX(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure) # tune hyperparameters @@ -99,32 +99,32 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_g, learner_m smpls = all_smpls[0] if tune_on_folds: - g_params, m_params, r_params = tune_nuisance_pliv_partial_x(y, x, d, z, - clone(learner_g), + 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_g'], + par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r']) else: xx = [(np.arange(len(y)), np.array([]))] - g_params, m_params, r_params = tune_nuisance_pliv_partial_x(y, x, d, z, - clone(learner_g), + 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_g'], + par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r']) - g_params = g_params * n_folds + 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_g), clone(learner_m), clone(learner_r), + clone(learner_l), clone(learner_m), clone(learner_r), all_smpls, dml_procedure, score, - g_params=g_params, m_params=m_params, r_params=r_params) + l_params=l_params, m_params=m_params, r_params=r_params) res_dict = {'coef': dml_pliv_obj.coef, 'coef_manual': res_manual['theta'], @@ -135,7 +135,7 @@ def dml_pliv_partial_x_fixture(generate_data_pliv_partialX, learner_g, learner_m for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv_partial_x(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], all_smpls, score, bootstrap, n_rep_boot) diff --git a/doubleml/tests/test_pliv_partial_xz.py b/doubleml/tests/test_pliv_partial_xz.py index 961929c8..1cff6904 100644 --- a/doubleml/tests/test_pliv_partial_xz.py +++ b/doubleml/tests/test_pliv_partial_xz.py @@ -39,15 +39,15 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner, score, dm # collect data obj_dml_data = generate_data_pliv_partialXZ - # Set machine learning methods for g, m & r - ml_g = clone(learner) + # Set machine learning methods for l, m & r + ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV._partialXZ(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure) dml_pliv_obj.fit() @@ -73,7 +73,7 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner, score, dm for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv_partial_xz(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], all_smpls, score, bootstrap, n_rep_boot) diff --git a/doubleml/tests/test_pliv_partial_xz_tune.py b/doubleml/tests/test_pliv_partial_xz_tune.py index 28f5739f..389fb418 100644 --- a/doubleml/tests/test_pliv_partial_xz_tune.py +++ b/doubleml/tests/test_pliv_partial_xz_tune.py @@ -15,7 +15,7 @@ @pytest.fixture(scope='module', params=[ElasticNet()]) -def learner_g(request): +def learner_l(request): return request.param @@ -59,9 +59,9 @@ def get_par_grid(learner): @pytest.fixture(scope='module') -def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_g, learner_m, learner_r, score, +def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_l, learner_m, learner_r, score, dml_procedure, tune_on_folds): - par_grid = {'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)} n_folds_tune = 4 @@ -73,15 +73,15 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_g, learner # collect data obj_dml_data = generate_data_pliv_partialXZ - # Set machine learning methods for g, m & r - ml_g = clone(learner_g) + # Set machine learning methods for l, m & r + ml_l = clone(learner_l) ml_m = clone(learner_m) ml_r = clone(learner_r) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV._partialXZ(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure) # tune hyperparameters @@ -99,32 +99,32 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_g, learner smpls = all_smpls[0] if tune_on_folds: - g_params, m_params, r_params = tune_nuisance_pliv_partial_xz(y, x, d, z, - clone(learner_g), + 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_g'], + par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r']) else: xx = [(np.arange(len(y)), np.arange(len(y)))] - g_params, m_params, r_params = tune_nuisance_pliv_partial_xz(y, x, d, z, - clone(learner_g), + 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_g'], + par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r']) - g_params = g_params * n_folds + 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_g), clone(learner_m), clone(learner_r), + clone(learner_l), clone(learner_m), clone(learner_r), all_smpls, dml_procedure, score, - g_params=g_params, m_params=m_params, r_params=r_params) + l_params=l_params, m_params=m_params, r_params=r_params) res_dict = {'coef': dml_pliv_obj.coef, 'coef_manual': res_manual['theta'], @@ -135,7 +135,7 @@ def dml_pliv_partial_xz_fixture(generate_data_pliv_partialXZ, learner_g, learner for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv_partial_xz(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], + res_manual['all_l_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], all_smpls, score, bootstrap, n_rep_boot) diff --git a/doubleml/tests/test_pliv_partial_z.py b/doubleml/tests/test_pliv_partial_z.py index ec586fa1..b182a61f 100644 --- a/doubleml/tests/test_pliv_partial_z.py +++ b/doubleml/tests/test_pliv_partial_z.py @@ -48,7 +48,7 @@ def dml_pliv_partial_z_fixture(generate_data_pliv_partialZ, learner, score, dml_ 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=n_folds, dml_procedure=dml_procedure) dml_pliv_obj.fit() diff --git a/doubleml/tests/test_pliv_partial_z_tune.py b/doubleml/tests/test_pliv_partial_z_tune.py index bba2bc96..751965f0 100644 --- a/doubleml/tests/test_pliv_partial_z_tune.py +++ b/doubleml/tests/test_pliv_partial_z_tune.py @@ -63,7 +63,7 @@ def dml_pliv_partial_z_fixture(generate_data_pliv_partialZ, learner_r, score, dm 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=n_folds, dml_procedure=dml_procedure) # tune hyperparameters diff --git a/doubleml/tests/test_pliv_tune.py b/doubleml/tests/test_pliv_tune.py index 43fde957..d2ff8c8b 100644 --- a/doubleml/tests/test_pliv_tune.py +++ b/doubleml/tests/test_pliv_tune.py @@ -15,7 +15,7 @@ @pytest.fixture(scope='module', params=[ElasticNet()]) -def learner_g(request): +def learner_l(request): return request.param @@ -59,8 +59,8 @@ def get_par_grid(learner): @pytest.fixture(scope='module') -def dml_pliv_fixture(generate_data_iv, learner_g, learner_m, learner_r, score, dml_procedure, tune_on_folds): - par_grid = {'ml_g': get_par_grid(learner_g), +def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, score, dml_procedure, 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 @@ -73,16 +73,16 @@ def dml_pliv_fixture(generate_data_iv, learner_g, learner_m, learner_r, score, d data = generate_data_iv x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for g, m & r - ml_g = clone(learner_g) + # Set machine learning methods for l, m & r + ml_l = clone(learner_l) ml_m = clone(learner_m) ml_r = clone(learner_r) np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'Z1') dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, - ml_g, ml_m, ml_r, - n_folds, + ml_l, ml_m, ml_r, + n_folds=n_folds, dml_procedure=dml_procedure) # tune hyperparameters @@ -100,23 +100,23 @@ def dml_pliv_fixture(generate_data_iv, learner_g, learner_m, learner_r, score, d smpls = all_smpls[0] if tune_on_folds: - g_params, m_params, r_params = tune_nuisance_pliv(y, x, d, z, - clone(learner_g), clone(learner_m), clone(learner_r), + l_params, m_params, r_params = tune_nuisance_pliv(y, x, d, z, + clone(learner_l), clone(learner_m), clone(learner_r), smpls, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], par_grid['ml_r']) + par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_r']) else: xx = [(np.arange(len(y)), np.array([]))] - g_params, m_params, r_params = tune_nuisance_pliv(y, x, d, z, - clone(learner_g), clone(learner_m), clone(learner_r), + l_params, m_params, r_params = tune_nuisance_pliv(y, x, d, z, + clone(learner_l), clone(learner_m), clone(learner_r), xx, n_folds_tune, - par_grid['ml_g'], par_grid['ml_m'], par_grid['ml_r']) - g_params = g_params * n_folds + 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(y, x, d, z, clone(learner_g), clone(learner_m), clone(learner_r), + res_manual = fit_pliv(y, x, d, z, clone(learner_l), clone(learner_m), clone(learner_r), all_smpls, dml_procedure, score, - g_params=g_params, m_params=m_params, r_params=r_params) + l_params=l_params, m_params=m_params, r_params=r_params) res_dict = {'coef': dml_pliv_obj.coef, 'coef_manual': res_manual['theta'], @@ -127,7 +127,7 @@ def dml_pliv_fixture(generate_data_iv, learner_g, learner_m, learner_r, score, d for bootstrap in boot_methods: np.random.seed(3141) boot_theta, boot_t_stat = boot_pliv(y, d, z, res_manual['thetas'], res_manual['ses'], - res_manual['all_g_hat'], res_manual['all_m_hat'], res_manual['all_r_hat'], + 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) From 80a0c6e4d73cf65c64d8345ea1971af2d95b9d34 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 17:36:51 +0200 Subject: [PATCH 23/47] implementation of the IV-type score for the PLIV model --- doubleml/double_ml_pliv.py | 181 ++++++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 25 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 6af5774f..fb023cf3 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -4,11 +4,31 @@ from sklearn.model_selection import GridSearchCV, RandomizedSearchCV from sklearn.linear_model import LinearRegression from sklearn.dummy import DummyRegressor +from sklearn.base import clone + +import warnings +from functools import wraps from .double_ml import DoubleML from ._utils import _dml_cv_predict, _dml_tune, _check_finite_predictions +# To be removed in version 0.6.0 +def changed_api_decorator(f): + @wraps(f) + def wrapper(*args, **kwds): + ml_l_missing = (len(set(kwds).intersection({'obj_dml_data', 'ml_l', 'ml_m', 'ml_r'})) + len(args)) < 5 + if ml_l_missing & ('ml_g' in kwds): + warnings.warn(("The required positional argument ml_g was renamed to ml_l. " + "Please adapt the argument name accordingly. " + "ml_g is redirected to ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + kwds['ml_l'] = kwds.pop('ml_g') + return f(*args, **kwds) + return wrapper + + class DoubleMLPLIV(DoubleML): """Double machine learning for partially linear IV regression models @@ -29,6 +49,13 @@ class DoubleMLPLIV(DoubleML): A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`r_0(X) = E[D|X]`. + ml_g : estimator implementing ``fit()`` and ``predict()`` + A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function + :math:`g_0(X) = E[Y - D \\theta_0|X]`. + Note: The learner `ml_g` is only required for the score ``'IV-type'``. Optionally, it can be specified and + estimated for callable scores. + n_folds : int Number of folds. Default is ``5``. @@ -39,7 +66,8 @@ class DoubleMLPLIV(DoubleML): score : str or callable A str (``'partialling out'`` is the only choice) specifying the score function - or a callable object / function with signature ``psi_a, psi_b = score(y, z, d, l_hat, m_hat, r_hat, smpls)``. + or a callable object / function with signature + ``psi_a, psi_b = score(y, z, d, l_hat, m_hat, r_hat, g_hat, smpls)``. Default is ``'partialling out'``. dml_procedure : str @@ -88,11 +116,13 @@ class DoubleMLPLIV(DoubleML): :math:`X = (X_1, \\ldots, X_p)` consists of other confounding covariates, and :math:`\\zeta` and :math:`V` are stochastic errors. """ + @changed_api_decorator def __init__(self, obj_dml_data, ml_l, ml_m, ml_r, + ml_g=None, n_folds=5, n_rep=1, score='partialling out', @@ -115,6 +145,18 @@ def __init__(self, _ = 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 isinstance(self.score, str) & (self.score == 'IV-type'): + if ml_g is None: + 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) + else: + _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + self._learner['ml_g'] = ml_g + else: + if callable(self.score) & (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_l': 'predict', 'ml_m': 'predict', 'ml_r': 'predict'} self._initialize_ml_nuisance_params() @@ -124,6 +166,7 @@ def _partialX(cls, ml_l, ml_m, ml_r, + ml_g=None, n_folds=5, n_rep=1, score='partialling out', @@ -134,6 +177,7 @@ def _partialX(cls, ml_l, ml_m, ml_r, + ml_g, n_folds, n_rep, score, @@ -167,6 +211,7 @@ def _partialZ(cls, DummyRegressor(), DummyRegressor(), ml_r, + None, n_folds, n_rep, score, @@ -199,6 +244,7 @@ def _partialXZ(cls, ml_l, ml_m, ml_r, + None, n_folds, n_rep, score, @@ -218,18 +264,12 @@ def _partialXZ(cls, return obj def _initialize_ml_nuisance_params(self): - if self.partialX & (not self.partialZ): - if self._dml_data.n_instr == 1: - valid_learner = ['ml_l', 'ml_m', 'ml_r'] - else: - valid_learner = ['ml_l', 'ml_r'] + ['ml_m_' + z_col for z_col in self._dml_data.z_cols] - elif (not self.partialX) & self.partialZ: - valid_learner = ['ml_r'] + 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] else: - assert (self.partialX & self.partialZ) - valid_learner = ['ml_l', 'ml_m', 'ml_r'] + param_names = self._learner.keys() self._params = {learner: {key: [None] * self.n_rep for key in self._dml_data.d_cols} - for learner in valid_learner} + for learner in param_names} def _check_score(self, score): if isinstance(score, str): @@ -254,6 +294,17 @@ def _check_data(self, obj_dml_data): 'use DoubleMLPLR instead of DoubleMLPLIV.') return + # To be removed in version 0.6.0 + def set_ml_nuisance_params(self, learner, treat_var, params): + if isinstance(self.score, str) & (self.score == 'partialling out') & (learner == 'ml_g'): + warnings.warn(("Learner ml_g was renamed to ml_l. " + "Please adapt the argument learner accordingly. " + "The provided parameters are set for ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + learner = 'ml_l' + super(DoubleMLPLIV, self).set_ml_nuisance_params(learner, treat_var, params) + def _nuisance_est(self, smpls, n_jobs_cv): if self.partialX & (not self.partialZ): psi_a, psi_b, preds = self._nuisance_est_partial_x(smpls, n_jobs_cv) @@ -315,14 +366,27 @@ def _nuisance_est_partial_x(self, smpls, n_jobs_cv): est_params=self._get_params('ml_r'), method=self._predict_method['ml_r']) _check_finite_predictions(r_hat, self._learner['ml_r'], 'ml_r', smpls) - psi_a, psi_b = self._score_elements(y, z, d, l_hat, m_hat, r_hat, smpls) + g_hat = 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 + 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) + # 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, self._learner['ml_g'], 'ml_g', smpls) + + psi_a, psi_b = self._score_elements(y, z, d, l_hat, m_hat, r_hat, g_hat, smpls) preds = {'ml_l': l_hat, 'ml_m': m_hat, - 'ml_r': r_hat} + 'ml_r': r_hat, + 'ml_g': g_hat} return psi_a, psi_b, preds - def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, smpls): + def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, g_hat, smpls): # compute residuals u_hat = y - l_hat w_hat = d - r_hat @@ -337,11 +401,16 @@ def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, smpls): r_hat_tilde = reg.predict(v_hat) if isinstance(self.score, str): - assert self.score == 'partialling out' if self._dml_data.n_instr == 1: - psi_a = -np.multiply(w_hat, v_hat) - psi_b = np.multiply(v_hat, u_hat) + 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' + psi_a = -np.multiply(v_hat, d) + psi_b = np.multiply(v_hat, y - g_hat) else: + assert self.score == 'partialling out' psi_a = -np.multiply(w_hat, r_hat_tilde) psi_b = np.multiply(r_hat_tilde, u_hat) else: @@ -352,7 +421,7 @@ def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, smpls): else: assert self._dml_data.n_instr == 1 psi_a, psi_b = self.score(y, z, d, - l_hat, m_hat, r_hat, smpls) + l_hat, m_hat, r_hat, g_hat, smpls) return psi_a, psi_b @@ -422,6 +491,39 @@ def _nuisance_est_partial_xz(self, smpls, n_jobs_cv): return psi_a, psi_b, preds + # To be removed in version 0.6.0 + 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): + + if isinstance(self.score, str) and (self.score == 'partialling out') and (param_grids is not None) and \ + ('ml_g' in param_grids) and ('ml_l' not in param_grids): + warnings.warn(("Learner ml_g was renamed to ml_l. " + "Please adapt the key of param_grids accordingly. " + "The provided param_grids for ml_g are set for ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + param_grids['ml_l'] = param_grids.pop('ml_g') + + if isinstance(self.score, str) and (self.score == 'partialling out') and (scoring_methods is not None) and \ + ('ml_g' in scoring_methods) and ('ml_l' not in scoring_methods): + warnings.warn(("Learner ml_g was renamed to ml_l. " + "Please adapt the key of scoring_methods accordingly. " + "The provided scoring_methods for ml_g are set for ml_l. " + "The redirection will be removed in a future version."), + DeprecationWarning, stacklevel=2) + scoring_methods['ml_l'] = scoring_methods.pop('ml_g') + + super(DoubleMLPLIV, self).tune(param_grids, tune_on_folds, scoring_methods, n_folds_tune, search_mode, + n_iter_randomized_search, n_jobs_cv, set_as_params, return_tune_res) + 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, @@ -470,15 +572,44 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold '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} else: m_best_params = [xx.best_params_ for xx in m_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} + # an ML model for g is obtained for the IV-type score and callable scores + 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) + r_hat = np.full_like(d, np.nan) + for idx, (train_index, _) in enumerate(smpls): + l_hat[train_index] = l_tune_res[idx].predict(x[train_index, :]) + m_hat[train_index] = m_tune_res[idx].predict(x[train_index, :]) + r_hat[train_index] = r_tune_res[idx].predict(x[train_index, :]) + 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_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} + 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} res = {'params': params, 'tune_res': tune_res} From ff944df5c3218bc532556d9d81eb798a9da1e1e8 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 17:38:37 +0200 Subject: [PATCH 24/47] remove assert; score could also be a callable --- doubleml/double_ml_plr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index eb7dd888..4df790e7 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -341,7 +341,6 @@ def _nuisance_tuning(self, smpls, param_grids, scoring_methods, n_folds_tune, n_ 'm_tune': m_tune_res, 'g_tune': g_tune_res} else: - assert self.score == 'partialling out' params = {'ml_l': l_best_params, 'ml_m': m_best_params} tune_res = {'l_tune': l_tune_res, From dc0ef90761345fbf968c64201199423976b6292a Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Mon, 2 May 2022 17:47:14 +0200 Subject: [PATCH 25/47] a couple more renamings ml_g to ml_l for consistency --- doubleml/tests/test_doubleml_exceptions.py | 44 +++++++++---------- .../test_doubleml_set_sample_splitting.py | 18 ++++---- doubleml/tests/test_multiway_cluster.py | 8 ++-- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index ba58bc1a..23388c32 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -33,13 +33,13 @@ def test_doubleml_exception_data(): msg = 'The data must be of DoubleMLData type.' with pytest.raises(TypeError, match=msg): - _ = DoubleMLPLR(pd.DataFrame(), ml_g, ml_m) + _ = DoubleMLPLR(pd.DataFrame(), ml_l, 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.') with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data_pliv, ml_g, ml_m) + _ = DoubleMLPLR(dml_data_pliv, ml_l, ml_m) # PLIV without IV msg = ('Incompatible data. ' @@ -105,10 +105,10 @@ def test_doubleml_exception_data(): def test_doubleml_exception_scores(): msg = 'Invalid score IV. Valid score IV-type or partialling out.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, score='IV') + _ = 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_g, ml_m, score=0) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, score=0) msg = 'Invalid score IV. Valid score ATE or ATTE.' with pytest.raises(ValueError, match=msg): @@ -175,47 +175,47 @@ def test_doubleml_exception_subgroups(): 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_g, ml_m, n_folds=1.5) + _ = 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.") with pytest.raises(TypeError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, n_rep=1.5) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_rep=1.5) msg = 'The number of folds must be positive. 0 was passed.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, n_folds=0) + _ = 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.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, n_rep=0) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, n_rep=0) msg = 'apply_cross_fitting must be True or False. Got 1.' with pytest.raises(TypeError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, apply_cross_fitting=1) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, apply_cross_fitting=1) msg = 'draw_sample_splitting must be True or False. Got true.' with pytest.raises(TypeError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, draw_sample_splitting='true') + _ = DoubleMLPLR(dml_data, ml_l, ml_m, draw_sample_splitting='true') @pytest.mark.ci def test_doubleml_exception_dml_procedure(): msg = 'dml_procedure must be "dml1" or "dml2". Got 1.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, dml_procedure='1') + _ = DoubleMLPLR(dml_data, ml_l, ml_m, dml_procedure='1') msg = 'dml_procedure must be "dml1" or "dml2". Got dml.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, dml_procedure='dml') + _ = DoubleMLPLR(dml_data, ml_l, ml_m, dml_procedure='dml') @pytest.mark.ci def test_doubleml_warning_crossfitting_onefold(): msg = 'apply_cross_fitting is set to False. Cross-fitting is not supported for n_folds = 1.' with pytest.warns(UserWarning, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, apply_cross_fitting=True, n_folds=1) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, apply_cross_fitting=True, n_folds=1) @pytest.mark.ci def test_doubleml_exception_no_cross_fit(): msg = 'Estimation without cross-fitting not supported for n_folds > 2.' with pytest.raises(AssertionError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, ml_m, apply_cross_fitting=False) + _ = DoubleMLPLR(dml_data, ml_l, ml_m, apply_cross_fitting=False) @pytest.mark.ci @@ -235,11 +235,11 @@ def test_doubleml_exception_get_params(): 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\(\).') - dml_plr_no_smpls = DoubleMLPLR(dml_data, ml_g, ml_m, draw_sample_splitting=False) + 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 msg = 'Sample splitting not specified. Draw samples via .draw_sample splitting().' - dml_pliv_cluster_no_smpls = DoubleMLPLIV(dml_cluster_data_pliv, ml_g, ml_m, ml_r, draw_sample_splitting=False) + dml_pliv_cluster_no_smpls = DoubleMLPLIV(dml_cluster_data_pliv, ml_l, ml_m, ml_r, draw_sample_splitting=False) with pytest.raises(ValueError, match=msg): _ = dml_pliv_cluster_no_smpls.smpls_cluster with pytest.raises(ValueError, match=msg): @@ -261,7 +261,7 @@ def test_doubleml_exception_fit(): @pytest.mark.ci def test_doubleml_exception_bootstrap(): - dml_plr_boot = DoubleMLPLR(dml_data, ml_g, ml_m) + dml_plr_boot = DoubleMLPLR(dml_data, ml_l, ml_m) msg = r'Apply fit\(\) before bootstrap\(\).' with pytest.raises(ValueError, match=msg): dml_plr_boot.bootstrap() @@ -280,7 +280,7 @@ def test_doubleml_exception_bootstrap(): @pytest.mark.ci def test_doubleml_exception_confint(): - dml_plr_confint = DoubleMLPLR(dml_data, ml_g, ml_m) + dml_plr_confint = DoubleMLPLR(dml_data, ml_l, ml_m) msg = 'joint must be True or False. Got 1.' with pytest.raises(TypeError, match=msg): @@ -308,7 +308,7 @@ def test_doubleml_exception_confint(): @pytest.mark.ci def test_doubleml_exception_p_adjust(): - dml_plr_p_adjust = DoubleMLPLR(dml_data, ml_g, ml_m) + dml_plr_p_adjust = DoubleMLPLR(dml_data, ml_l, ml_m) msg = r'Apply fit\(\) before p_adjust\(\).' with pytest.raises(ValueError, match=msg): @@ -457,7 +457,7 @@ def test_doubleml_exception_learner(): with pytest.warns(UserWarning): _ = DoubleMLIRM(dml_data_irm, Lasso(), _DummyNoClassifier()) - # ToDo: Currently for ml_g (and others) we only check whether the learner can be identified as regressor. However, + # 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.' with pytest.warns(UserWarning, match=msg): @@ -608,7 +608,7 @@ def test_doubleml_nan_prediction(): msg = r'Predictions from learner LassoWithNanPred\(\) for ml_m are not finite.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, LassoWithNanPred()).fit() + _ = DoubleMLPLR(dml_data, ml_l, LassoWithNanPred()).fit() msg = r'Predictions from learner LassoWithInfPred\(\) for ml_m are not finite.' with pytest.raises(ValueError, match=msg): - _ = DoubleMLPLR(dml_data, ml_g, LassoWithInfPred()).fit() + _ = DoubleMLPLR(dml_data, ml_l, LassoWithInfPred()).fit() diff --git a/doubleml/tests/test_doubleml_set_sample_splitting.py b/doubleml/tests/test_doubleml_set_sample_splitting.py index fa7d65f7..e05436e4 100644 --- a/doubleml/tests/test_doubleml_set_sample_splitting.py +++ b/doubleml/tests/test_doubleml_set_sample_splitting.py @@ -8,9 +8,9 @@ np.random.seed(3141) dml_data = make_plr_CCDDHNR2018(n_obs=10) -ml_g = Lasso() +ml_l = Lasso() ml_m = Lasso() -dml_plr = DoubleMLPLR(dml_data, ml_g, ml_m, +dml_plr = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=7, n_rep=8, draw_sample_splitting=False) @@ -172,9 +172,9 @@ def test_doubleml_set_sample_splitting_all_list(): @pytest.mark.ci def test_doubleml_draw_vs_set(): np.random.seed(3141) - dml_plr_set = DoubleMLPLR(dml_data, ml_g, ml_m, n_folds=7, n_rep=8) + dml_plr_set = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=7, n_rep=8) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_g, ml_m, + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=1, n_rep=1, apply_cross_fitting=False) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) @@ -183,7 +183,7 @@ def test_doubleml_draw_vs_set(): dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls[0][0]) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_g, ml_m, + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=2, n_rep=1, apply_cross_fitting=False) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) @@ -192,26 +192,26 @@ def test_doubleml_draw_vs_set(): dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls[0][0]) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) - dml_plr_drawn = DoubleMLPLR(dml_data, ml_g, ml_m, + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=2, n_rep=1, apply_cross_fitting=True) 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_g, ml_m, + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=5, n_rep=1, apply_cross_fitting=True) 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_g, ml_m, + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=5, n_rep=3, apply_cross_fitting=True) 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_g, ml_m, + dml_plr_drawn = DoubleMLPLR(dml_data, ml_l, ml_m, n_folds=2, n_rep=4, apply_cross_fitting=False) dml_plr_set.set_sample_splitting(dml_plr_drawn.smpls) _assert_resampling_pars(dml_plr_drawn, dml_plr_set) diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index 9e18f28b..fe88ca5a 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -263,14 +263,14 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_g = clone(learner) + # 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) np.random.seed(3141) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, - ml_g, ml_m, + ml_l, ml_m, n_folds, dml_procedure=dml_procedure) dml_plr_obj.fit() @@ -283,7 +283,7 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): cluster_cols='index') np.random.seed(3141) dml_plr_cluster_obj = dml.DoubleMLPLR(dml_cluster_data, - ml_g, ml_m, + ml_l, ml_m, n_folds, dml_procedure=dml_procedure) dml_plr_cluster_obj.fit() From 58a4674f7455c048a8c0f5c067f917ae8a0b33d6 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Tue, 3 May 2022 11:42:09 +0200 Subject: [PATCH 26/47] refactor the check and set learner part in the initializer --- doubleml/double_ml_plr.py | 59 ++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 4df790e7..a33a9d54 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -130,7 +130,33 @@ def __init__(self, self._check_data(self._dml_data) self._check_score(self.score) - self._check_and_set_learner(ml_l, ml_m, ml_g) + + _ = 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: + _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): + self._learner['ml_g'] = ml_g + # Question: Add a warning when ml_g is set for partialling out score where it is not required / used? + 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' + 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.') + else: + self._predict_method['ml_m'] = 'predict' + self._initialize_ml_nuisance_params() def _initialize_ml_nuisance_params(self): @@ -157,37 +183,6 @@ def _check_data(self, obj_dml_data): 'To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR.') return - def _check_and_set_learner(self, ml_l, ml_m, ml_g): - _ = 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 isinstance(self.score, str) & (self.score == 'IV-type'): - if ml_g is None: - 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) - else: - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner['ml_g'] = ml_g - else: - if callable(self.score) & (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_l': 'predict'} - if ml_m_is_classifier: - if self._dml_data.binary_treats.all(): - 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.') - else: - self._predict_method['ml_m'] = 'predict' - if 'ml_g' in self._learner: - self._predict_method['ml_g'] = 'predict' - - return - # To be removed in version 0.6.0 def set_ml_nuisance_params(self, learner, treat_var, params): if isinstance(self.score, str) & (self.score == 'partialling out') & (learner == 'ml_g'): From 1dadf61376fbd673e734128c79beadaac33c566e Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Tue, 3 May 2022 11:44:05 +0200 Subject: [PATCH 27/47] pass n_folds as keyword argument to fix unit tests --- doubleml/tests/test_multiway_cluster.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index fe88ca5a..5edfd80f 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -69,7 +69,7 @@ def dml_pliv_multiway_cluster_old_vs_new_fixture(generate_data_iv, learner): dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, ml_g, ml_m, ml_r, - n_folds, + n_folds=n_folds, dml_procedure=dml_procedure, draw_sample_splitting=False) dml_pliv_obj.set_sample_splitting(smpls_lin_ind) @@ -79,7 +79,7 @@ def dml_pliv_multiway_cluster_old_vs_new_fixture(generate_data_iv, learner): np.random.seed(3141) dml_pliv_obj_cluster = dml.DoubleMLPLIV(obj_dml_cluster_data, ml_g, ml_m, ml_r, - n_folds, + n_folds=n_folds, dml_procedure=dml_procedure) dml_pliv_obj_cluster.fit() @@ -110,7 +110,7 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, dml_procedure): np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_cluster_data, ml_g, ml_m, ml_r, - n_folds, + n_folds=n_folds, n_rep=n_rep, score=score, dml_procedure=dml_procedure) @@ -196,7 +196,7 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, dml_procedure): np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_oneway_cluster_data, ml_g, ml_m, ml_r, - n_folds, + n_folds=n_folds, score=score, dml_procedure=dml_procedure) @@ -271,7 +271,7 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): np.random.seed(3141) dml_plr_obj = dml.DoubleMLPLR(obj_dml_data, ml_l, ml_m, - n_folds, + n_folds=n_folds, dml_procedure=dml_procedure) dml_plr_obj.fit() @@ -284,7 +284,7 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): np.random.seed(3141) dml_plr_cluster_obj = dml.DoubleMLPLR(dml_cluster_data, ml_l, ml_m, - n_folds, + n_folds=n_folds, dml_procedure=dml_procedure) dml_plr_cluster_obj.fit() From cbf3e02dff5c371a5510f33c3e56c54c592306b3 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Tue, 3 May 2022 11:56:47 +0200 Subject: [PATCH 28/47] refactor the check and set learner part of the initializer --- doubleml/double_ml_pliv.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index fb023cf3..54dd4ec4 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -145,19 +145,18 @@ def __init__(self, _ = 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 isinstance(self.score, str) & (self.score == 'IV-type'): - if ml_g is None: - 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) - else: - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) - self._learner['ml_g'] = ml_g - else: - if callable(self.score) & (ml_g is not None): - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + if ml_g is not None: + _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) + if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score): self._learner['ml_g'] = ml_g + # Question: Add a warning when ml_g is set for partialling out score where it is not required / used? + 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', 'ml_m': 'predict', 'ml_r': 'predict'} + if 'ml_g' in self._learner: + self._predict_method['ml_g'] = 'predict' self._initialize_ml_nuisance_params() @classmethod From 5b3bca8c543b706a44168744894d31a69930e316 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Tue, 3 May 2022 16:42:09 +0200 Subject: [PATCH 29/47] finalize the implementation of the IV-type score for PLIV: Manual impl and unit tests --- doubleml/double_ml_pliv.py | 3 +- doubleml/tests/_utils_pliv_manual.py | 152 ++++++++++++++++------- doubleml/tests/test_pliv.py | 26 ++-- doubleml/tests/test_pliv_no_cross_fit.py | 14 ++- doubleml/tests/test_pliv_tune.py | 54 +++++--- 5 files changed, 171 insertions(+), 78 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 54dd4ec4..ec875690 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -533,7 +533,8 @@ def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_fold if scoring_methods is None: scoring_methods = {'ml_l': None, 'ml_m': None, - 'ml_r': 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, diff --git a/doubleml/tests/_utils_pliv_manual.py b/doubleml/tests/_utils_pliv_manual.py index 679d2ddb..35ee8618 100644 --- a/doubleml/tests/_utils_pliv_manual.py +++ b/doubleml/tests/_utils_pliv_manual.py @@ -1,12 +1,13 @@ import numpy as np +from sklearn.base import clone from ._utils_boot import boot_manual, draw_weights from ._utils import fit_predict, tune_grid_search def fit_pliv(y, x, d, z, - learner_l, learner_m, learner_r, all_smpls, dml_procedure, score, - n_rep=1, l_params=None, m_params=None, r_params=None): + learner_l, learner_m, learner_r, learner_g, all_smpls, dml_procedure, 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) @@ -14,28 +15,31 @@ def fit_pliv(y, x, d, z, all_l_hat = list() all_m_hat = list() all_r_hat = list() + all_g_hat = list() for i_rep in range(n_rep): smpls = all_smpls[i_rep] - l_hat, m_hat, r_hat = fit_nuisance_pliv(y, x, d, z, - learner_l, learner_m, learner_r, - smpls, - l_params, m_params, r_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) if dml_procedure == 'dml1': thetas[i_rep], ses[i_rep] = pliv_dml1(y, x, d, z, - l_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, g_hat, smpls, score) else: assert dml_procedure == 'dml2' thetas[i_rep], ses[i_rep] = pliv_dml2(y, x, d, z, - l_hat, m_hat, r_hat, + l_hat, m_hat, r_hat, g_hat, smpls, score) theta = np.median(thetas) @@ -43,94 +47,140 @@ def fit_pliv(y, x, d, z, 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_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, smpls, l_params=None, m_params=None, r_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) r_hat = fit_predict(d, x, ml_r, r_params, smpls) - return l_hat, m_hat, r_hat + 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) + 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) + + ml_g = clone(ml_g) + g_hat = fit_predict(y - theta_initial * d, x, ml_g, g_params, smpls) + else: + g_hat = [] + + return l_hat, m_hat, r_hat, g_hat -def tune_nuisance_pliv(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(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) r_tune_res = tune_grid_search(d, x, ml_r, smpls, param_grid_r, n_folds_tune) + if tune_g: + l_hat = np.full_like(y, np.nan) + m_hat = np.full_like(z, np.nan) + r_hat = np.full_like(d, np.nan) + for idx, (train_index, _) in enumerate(smpls): + l_hat[train_index] = l_tune_res[idx].predict(x[train_index, :]) + m_hat[train_index] = m_tune_res[idx].predict(x[train_index, :]) + r_hat[train_index] = r_tune_res[idx].predict(x[train_index, :]) + 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 = 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 = [] + 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] - return l_best_params, m_best_params, r_best_params + return l_best_params, m_best_params, r_best_params, g_best_params -def compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls): - u_hat = np.full_like(y, np.nan, dtype='float64') - v_hat = np.full_like(z, np.nan, dtype='float64') - w_hat = np.full_like(d, np.nan, dtype='float64') +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') for idx, (_, test_index) in enumerate(smpls): - u_hat[test_index] = y[test_index] - l_hat[idx] - v_hat[test_index] = z[test_index] - m_hat[idx] - w_hat[test_index] = d[test_index] - r_hat[idx] + y_minus_l_hat[test_index] = y[test_index] - l_hat[idx] + z_minus_m_hat[test_index] = z[test_index] - m_hat[idx] + d_minus_r_hat[test_index] = d[test_index] - r_hat[idx] + if len(g_hat) > 0: + y_minus_g_hat[test_index] = y[test_index] - g_hat[idx] - return u_hat, v_hat, w_hat + return y_minus_l_hat, z_minus_m_hat, d_minus_r_hat, y_minus_g_hat -def pliv_dml1(y, x, d, z, l_hat, m_hat, r_hat, smpls, score): +def pliv_dml1(y, x, d, z, l_hat, m_hat, r_hat, g_hat, smpls, score): thetas = np.zeros(len(smpls)) n_obs = len(y) - u_hat, v_hat, w_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, y_minus_g_hat = compute_pliv_residuals( + y, d, z, l_hat, m_hat, r_hat, g_hat, smpls) for idx, (_, test_index) in enumerate(smpls): - thetas[idx] = pliv_orth(u_hat[test_index], v_hat[test_index], w_hat[test_index], d[test_index], score) + thetas[idx] = pliv_orth(y_minus_l_hat[test_index], z_minus_m_hat[test_index], + d_minus_r_hat[test_index], y_minus_g_hat[test_index], + d[test_index], score) theta_hat = np.mean(thetas) if len(smpls) > 1: - se = np.sqrt(var_pliv(theta_hat, d, u_hat, v_hat, w_hat, score, n_obs)) + 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)) else: assert len(smpls) == 1 test_index = smpls[0][1] n_obs = len(test_index) se = np.sqrt(var_pliv(theta_hat, d[test_index], - u_hat[test_index], v_hat[test_index], w_hat[test_index], + y_minus_l_hat[test_index], z_minus_m_hat[test_index], + d_minus_r_hat[test_index], y_minus_g_hat[test_index], score, n_obs)) return theta_hat, se -def pliv_dml2(y, x, d, z, l_hat, m_hat, r_hat, smpls, score): +def pliv_dml2(y, x, d, z, l_hat, m_hat, r_hat, g_hat, smpls, score): n_obs = len(y) - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls) - theta_hat = pliv_orth(u_hat, v_hat, w_hat, d, score) - se = np.sqrt(var_pliv(theta_hat, d, u_hat, v_hat, w_hat, score, n_obs)) + 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) + 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)) return theta_hat, se -def var_pliv(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)) +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)) + 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)) return var -def pliv_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)) +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)) + 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)) return res -def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, +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_theta = list() @@ -144,7 +194,7 @@ def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, n_obs = len(test_index) weights = draw_weights(bootstrap, n_rep_boot, n_obs) boot_theta, 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], smpls, + 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_theta.append(boot_theta) all_boot_t_stat.append(boot_t_stat) @@ -155,18 +205,30 @@ def boot_pliv(y, d, z, thetas, ses, all_l_hat, all_m_hat, all_r_hat, return boot_theta, boot_t_stat -def boot_pliv_single_split(theta, y, d, z, l_hat, m_hat, r_hat, +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): - assert score == 'partialling out' - u_hat, v_hat, w_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, y_minus_g_hat = compute_pliv_residuals( + y, d, z, l_hat, m_hat, r_hat, g_hat, smpls) if apply_cross_fitting: - J = np.mean(-np.multiply(v_hat, w_hat)) + if score == 'partialling out': + J = np.mean(-np.multiply(z_minus_m_hat, d_minus_r_hat)) + else: + assert score == 'IV-type' + J = np.mean(-np.multiply(z_minus_m_hat, d)) else: test_index = smpls[0][1] - J = np.mean(-np.multiply(v_hat[test_index], w_hat[test_index])) + 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' + J = np.mean(-np.multiply(z_minus_m_hat[test_index], d[test_index])) - psi = np.multiply(u_hat - w_hat*theta, v_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) boot_theta, boot_t_stat = boot_manual(psi, J, smpls, se, weights, n_rep_boot, apply_cross_fitting) diff --git a/doubleml/tests/test_pliv.py b/doubleml/tests/test_pliv.py index 9e0dbb86..34fdca8b 100644 --- a/doubleml/tests/test_pliv.py +++ b/doubleml/tests/test_pliv.py @@ -22,7 +22,7 @@ def learner(request): @pytest.fixture(scope='module', - params=['partialling out']) + params=['partialling out', 'IV-type']) def score(request): return request.param @@ -43,17 +43,27 @@ def dml_pliv_fixture(generate_data_iv, learner, score, dml_procedure): data = generate_data_iv x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for l, m & r + # Set machine learning methods for l, m, r & g ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) + ml_g = clone(learner) 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, - n_folds=n_folds, - dml_procedure=dml_procedure) + if score == 'partialling out': + dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_data, + ml_l, ml_m, ml_r, + n_folds=n_folds, + score=score, + dml_procedure=dml_procedure) + 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, + dml_procedure=dml_procedure) dml_pliv_obj.fit() @@ -66,7 +76,8 @@ def dml_pliv_fixture(generate_data_iv, learner, score, dml_procedure): all_smpls = draw_smpls(n_obs, n_folds) res_manual = fit_pliv(y, x, d, z, - clone(learner), clone(learner), clone(learner), all_smpls, dml_procedure, score) + clone(learner), clone(learner), clone(learner), clone(learner), + all_smpls, dml_procedure, score) res_dict = {'coef': dml_pliv_obj.coef, 'coef_manual': res_manual['theta'], @@ -78,6 +89,7 @@ def dml_pliv_fixture(generate_data_iv, learner, score, dml_procedure): np.random.seed(3141) boot_theta, 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) diff --git a/doubleml/tests/test_pliv_no_cross_fit.py b/doubleml/tests/test_pliv_no_cross_fit.py index ade7a74b..35c03053 100644 --- a/doubleml/tests/test_pliv_no_cross_fit.py +++ b/doubleml/tests/test_pliv_no_cross_fit.py @@ -19,7 +19,7 @@ def learner(request): @pytest.fixture(scope='module', - params=['partialling out']) + params=['partialling out', 'IV-type']) def score(request): return request.param @@ -40,16 +40,18 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): data = generate_data_iv x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for l, m & r + # Set machine learning methods for l, m, r & g ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) + ml_g = clone(learner) 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_l, ml_m, ml_r, ml_g, n_folds=n_folds, + score=score, dml_procedure=dml_procedure, apply_cross_fitting=False) @@ -69,7 +71,8 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): smpls = [smpls[0]] res_manual = fit_pliv(y, x, d, z, - clone(learner), clone(learner), clone(learner), [smpls], dml_procedure, score) + clone(learner), clone(learner), clone(learner), clone(learner), + [smpls], dml_procedure, score) res_dict = {'coef': dml_pliv_obj.coef, 'coef_manual': res_manual['theta'], @@ -80,7 +83,8 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): for bootstrap in boot_methods: np.random.seed(3141) boot_theta, 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_l_hat'], res_manual['all_m_hat'], + res_manual['all_r_hat'], res_manual['all_g_hat'], [smpls], score, bootstrap, n_rep_boot, apply_cross_fitting=False) diff --git a/doubleml/tests/test_pliv_tune.py b/doubleml/tests/test_pliv_tune.py index d2ff8c8b..efb70862 100644 --- a/doubleml/tests/test_pliv_tune.py +++ b/doubleml/tests/test_pliv_tune.py @@ -4,8 +4,7 @@ from sklearn.base import clone -from sklearn.linear_model import ElasticNet -from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import Lasso, ElasticNet import doubleml as dml @@ -14,7 +13,8 @@ @pytest.fixture(scope='module', - params=[ElasticNet()]) + params=[Lasso(), + ElasticNet()]) def learner_l(request): return request.param @@ -32,7 +32,13 @@ def learner_r(request): @pytest.fixture(scope='module', - params=['partialling out']) + params=[ElasticNet()]) +def learner_g(request): + return request.param + + +@pytest.fixture(scope='module', + params=['partialling out', 'IV-type']) def score(request): return request.param @@ -50,8 +56,8 @@ def tune_on_folds(request): def get_par_grid(learner): - if learner.__class__ == RandomForestRegressor: - par_grid = {'n_estimators': [5, 10, 20]} + if learner.__class__ == Lasso: + par_grid = {'alpha': np.linspace(0.05, .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)} @@ -59,10 +65,11 @@ def get_par_grid(learner): @pytest.fixture(scope='module') -def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, score, dml_procedure, tune_on_folds): +def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_g, score, dml_procedure, 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_r': get_par_grid(learner_r), + 'ml_g': get_par_grid(learner_g)} n_folds_tune = 4 boot_methods = ['Bayes', 'normal', 'wild'] @@ -77,12 +84,14 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, score, d ml_l = clone(learner_l) ml_m = clone(learner_m) ml_r = clone(learner_r) + ml_g = clone(learner_g) 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_l, ml_m, ml_r, ml_g, n_folds=n_folds, + score=score, dml_procedure=dml_procedure) # tune hyperparameters @@ -100,23 +109,27 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, score, d smpls = all_smpls[0] if tune_on_folds: - l_params, m_params, r_params = tune_nuisance_pliv(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, 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']) else: xx = [(np.arange(len(y)), np.array([]))] - l_params, m_params, r_params = tune_nuisance_pliv(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, 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']) + 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), + res_manual = fit_pliv(y, x, d, z, clone(learner_l), clone(learner_m), clone(learner_r), clone(learner_g), all_smpls, dml_procedure, score, - l_params=l_params, m_params=m_params, r_params=r_params) + l_params=l_params, m_params=m_params, r_params=r_params, g_params=g_params) res_dict = {'coef': dml_pliv_obj.coef, 'coef_manual': res_manual['theta'], @@ -127,7 +140,8 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, score, d for bootstrap in boot_methods: np.random.seed(3141) boot_theta, 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_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) From 4b6f9a09a9a4db741d04ef9ddd98ff77a09202cd Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Tue, 3 May 2022 16:57:54 +0200 Subject: [PATCH 30/47] add IV-type score also to the unit tests for the multiway clustering PLIV --- doubleml/tests/test_multiway_cluster.py | 97 +++++++++++++++++-------- 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index 3c3d4782..ee677210 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -39,6 +39,12 @@ def learner(request): return request.param +@pytest.fixture(scope='module', + params=['partialling out', 'IV-type']) +def score(request): + return request.param + + @pytest.fixture(scope='module', params=['dml1', 'dml2']) def dml_procedure(request): @@ -97,19 +103,19 @@ def test_dml_pliv_multiway_cluster_old_vs_new_coef(dml_pliv_multiway_cluster_old @pytest.fixture(scope='module') -def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, dml_procedure): +def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score, dml_procedure): n_folds = 2 n_rep = 2 - score = 'partialling out' - # Set machine learning methods for l, m & r + # Set machine learning methods for l, m, r & g ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) + ml_g = clone(learner) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_cluster_data, - ml_l, ml_m, ml_r, + ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, n_rep=n_rep, score=score, @@ -125,7 +131,7 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, dml_procedure): z = np.ravel(obj_dml_cluster_data.z) res_manual = fit_pliv(y, x, d, z, - clone(learner), clone(learner), clone(learner), + clone(learner), clone(learner), clone(learner), clone(learner), dml_pliv_obj.smpls, dml_procedure, score, n_rep=n_rep) thetas = np.full(n_rep, np.nan) @@ -134,19 +140,35 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, dml_procedure): 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] - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls_one_split) - - psi_a = -np.multiply(v_hat, w_hat) - if dml_procedure == 'dml2': - psi_b = np.multiply(v_hat, u_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) + 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) + + if score == 'partialling out': + psi_a = -np.multiply(z_minus_m_hat, d_minus_r_hat) + if dml_procedure == 'dml2': + 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) + else: + theta = res_manual['thetas'][i_rep] + psi = np.multiply(y_minus_l_hat - d_minus_r_hat * theta, z_minus_m_hat) else: - theta = res_manual['thetas'][i_rep] - psi = np.multiply(u_hat - w_hat * theta, v_hat) + assert score == 'IV-type' + psi_a = -np.multiply(z_minus_m_hat, d) + if dml_procedure == 'dml2': + 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) + else: + theta = res_manual['thetas'][i_rep] + 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], @@ -184,18 +206,18 @@ def test_dml_pliv_multiway_cluster_se(dml_pliv_multiway_cluster_fixture): @pytest.fixture(scope='module') -def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, dml_procedure): +def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, score, dml_procedure): n_folds = 3 - score = 'partialling out' # Set machine learning methods for l, m & r ml_l = clone(learner) ml_m = clone(learner) ml_r = clone(learner) + ml_g = clone(learner) np.random.seed(3141) dml_pliv_obj = dml.DoubleMLPLIV(obj_dml_oneway_cluster_data, - ml_l, ml_m, ml_r, + ml_l, ml_m, ml_r, ml_g, n_folds=n_folds, score=score, dml_procedure=dml_procedure) @@ -210,23 +232,38 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, dml_procedure): 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), clone(learner), clone(learner), clone(learner), dml_pliv_obj.smpls, dml_procedure, 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] - u_hat, v_hat, w_hat = compute_pliv_residuals(y, d, z, l_hat, m_hat, r_hat, smpls_one_split) - - psi_a = -np.multiply(v_hat, w_hat) - if dml_procedure == 'dml2': - psi_b = np.multiply(v_hat, u_hat) - theta = est_one_way_cluster_dml2(psi_a, psi_b, - obj_dml_oneway_cluster_data.cluster_vars[:, 0], - smpls_one_split) + 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) + + if score == 'partialling out': + psi_a = -np.multiply(z_minus_m_hat, d_minus_r_hat) + if dml_procedure == 'dml2': + 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) + else: + theta = res_manual['theta'] + psi = np.multiply(y_minus_l_hat - d_minus_r_hat * theta, z_minus_m_hat) else: - theta = res_manual['theta'] - psi = np.multiply(u_hat - w_hat * theta, v_hat) + assert score == 'IV-type' + psi_a = -np.multiply(z_minus_m_hat, d) + if dml_procedure == 'dml2': + 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) + else: + theta = res_manual['theta'] + 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) From 2ddabc3a1dccdee39d52bed4bb323e5b0d469cf1 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Tue, 3 May 2022 18:04:28 +0200 Subject: [PATCH 31/47] add unit tests for the deprecation warnings for the PLIV model; ml_g to ml_l --- doubleml/tests/test_doubleml_exceptions.py | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 23388c32..ef333ee8 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -18,9 +18,11 @@ 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_data_pliv = make_pliv_CHS2015(n_obs=50, dim_z=1) +dml_pliv = DoubleMLPLIV(dml_data_pliv, ml_l, ml_m, ml_r) + dml_data_irm = make_irm_data(n_obs=50) dml_data_iivm = make_iivm_data(n_obs=50) -dml_data_pliv = make_pliv_CHS2015(n_obs=50, dim_z=1) dml_cluster_data_pliv = make_pliv_multiway_cluster_CKMS2021(N=10, M=10) (x, y, d, z) = make_iivm_data(n_obs=50, return_type="array") y[y > 0] = 1 @@ -355,6 +357,19 @@ def test_doubleml_exception_tune(): scoring_methods={'ml_g': 'explained_variance', 'ml_m': 'explained_variance'}) + msg = 'Learner ml_g was renamed to ml_l. ' + with pytest.warns(DeprecationWarning, match=msg): + dml_pliv.tune({'ml_g': {'alpha': [0.05, 0.5]}, + 'ml_m': {'alpha': [0.05, 0.5]}, + 'ml_r': {'alpha': [0.05, 0.5]}}) + with pytest.warns(DeprecationWarning, match=msg): + dml_pliv.tune({'ml_l': {'alpha': [0.05, 0.5]}, + 'ml_m': {'alpha': [0.05, 0.5]}, + 'ml_r': {'alpha': [0.05, 0.5]}}, + scoring_methods={'ml_g': 'explained_variance', + 'ml_m': 'explained_variance', + 'ml_r': 'explained_variance'}) + 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. ' @@ -473,6 +488,10 @@ def test_doubleml_exception_learner(): with pytest.warns(DeprecationWarning, match=msg): _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) + msg = 'ml_g was renamed to ml_l' + with pytest.warns(DeprecationWarning, match=msg): + _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) + # 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.') @@ -536,6 +555,10 @@ def test_doubleml_exception_learner(): with pytest.warns(DeprecationWarning, match=msg): dml_plr.set_ml_nuisance_params('ml_g', 'd', {'max_iter': 314}) + msg = 'Learner ml_g was renamed to ml_l. ' + with pytest.warns(DeprecationWarning, match=msg): + dml_pliv.set_ml_nuisance_params('ml_g', 'd', {'max_iter': 314}) + @pytest.mark.ci @pytest.mark.filterwarnings("ignore:Learner provided for") From bce9aad5025c45943e5a087a12973bf732f77bb8 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 5 May 2022 16:10:13 +0200 Subject: [PATCH 32/47] fix notation in docu --- doubleml/double_ml_pliv.py | 2 +- doubleml/double_ml_plr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index ec875690..2098caad 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -39,7 +39,7 @@ class DoubleMLPLIV(DoubleML): ml_l : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`l_0(X) = E[Y|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`\ell_0(X) = E[Y|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index a33a9d54..f98caa33 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -36,7 +36,7 @@ class DoubleMLPLR(DoubleML): ml_l : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`l_0(X) = E[Y|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`\ell_0(X) = E[Y|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. From 981acaa386dd0e4afd1ba96d5a3181d0fdd0dac5 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 5 May 2022 16:11:01 +0200 Subject: [PATCH 33/47] fix notation in docu --- doubleml/double_ml_plr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index a33a9d54..65cdb953 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -36,7 +36,7 @@ class DoubleMLPLR(DoubleML): ml_l : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`l_0(X) = E[Y|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`\\ell_0(X) = E[Y|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. From 577154f8def9269d8b63e13f384ab64c47d5da87 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 5 May 2022 16:12:15 +0200 Subject: [PATCH 34/47] fix notation in docu --- doubleml/double_ml_pliv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 2098caad..387be3b4 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -39,7 +39,7 @@ class DoubleMLPLIV(DoubleML): ml_l : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. - :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`\ell_0(X) = E[Y|X]`. + :py:class:`sklearn.ensemble.RandomForestRegressor`) for the nuisance function :math:`\\ell_0(X) = E[Y|X]`. ml_m : estimator implementing ``fit()`` and ``predict()`` A machine learner implementing ``fit()`` and ``predict()`` methods (e.g. From 1fca7597c831f1b61ebcbe9b956fedfc5a7e544d Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Thu, 5 May 2022 16:23:21 +0200 Subject: [PATCH 35/47] provide all arguments as keyword arguments to callable scores --- doubleml/double_ml_iivm.py | 4 +++- doubleml/double_ml_irm.py | 4 +++- doubleml/double_ml_pliv.py | 5 +++-- doubleml/double_ml_plr.py | 4 +++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/doubleml/double_ml_iivm.py b/doubleml/double_ml_iivm.py index 29313bbd..b95fd069 100644 --- a/doubleml/double_ml_iivm.py +++ b/doubleml/double_ml_iivm.py @@ -308,7 +308,9 @@ def _score_elements(self, y, z, d, g_hat0, g_hat1, m_hat, r_hat0, r_hat1, smpls) - np.divide(np.multiply(1.0-z, w_hat0), 1.0 - m_hat)) else: assert callable(self.score) - psi_a, psi_b = self.score(y, z, d, g_hat0, g_hat1, m_hat, r_hat0, r_hat1, 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, r_hat0=r_hat0, r_hat1=r_hat1, + smpls=smpls) return psi_a, psi_b diff --git a/doubleml/double_ml_irm.py b/doubleml/double_ml_irm.py index ccdb2683..4ecd2b78 100644 --- a/doubleml/double_ml_irm.py +++ b/doubleml/double_ml_irm.py @@ -254,7 +254,9 @@ def _score_elements(self, y, d, g_hat0, g_hat1, m_hat, smpls): psi_a = - np.divide(d, p_hat) else: assert callable(self.score) - psi_a, psi_b = self.score(y, d, g_hat0, g_hat1, m_hat, smpls) + psi_a, psi_b = self.score(y=y, d=d, + g_hat0=g_hat0, g_hat1=g_hat1, m_hat=m_hat, + smpls=smpls) return psi_a, psi_b diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 387be3b4..cc3c31e9 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -419,8 +419,9 @@ def _score_elements(self, y, z, d, l_hat, m_hat, r_hat, g_hat, smpls): 'with several instruments.') else: assert self._dml_data.n_instr == 1 - psi_a, psi_b = self.score(y, z, d, - l_hat, m_hat, r_hat, g_hat, 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 diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index 65cdb953..e54ac4cb 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -253,7 +253,9 @@ def _score_elements(self, y, d, l_hat, m_hat, g_hat, smpls): psi_b = np.multiply(v_hat, u_hat) else: assert callable(self.score) - psi_a, psi_b = self.score(y, d, l_hat, m_hat, g_hat, 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 From 51d64c11d6ec7bcf2a8828b76647fc8fcd5149f0 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 09:14:10 +0200 Subject: [PATCH 36/47] docu update --- doubleml/double_ml_pliv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 8655ec34..7b7cf762 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -65,7 +65,7 @@ class DoubleMLPLIV(DoubleML): Default is ``1``. score : str or callable - A str (``'partialling out'`` is the only choice) specifying the score function + A str (``'partialling out'`` or ``'IV-type'``) specifying the score function or a callable object / function with signature ``psi_a, psi_b = score(y, z, d, l_hat, m_hat, r_hat, g_hat, smpls)``. Default is ``'partialling out'``. From bfdb9fc3c892bca1d308e540c7100a2ea5289d5f Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 10:06:45 +0200 Subject: [PATCH 37/47] IV type score is new for PLIV; no need to warn; instead throw an exception if not all four learners ml_l, ml_m, ml_r and ml_g are set --- doubleml/double_ml_pliv.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 7b7cf762..5ebbe1f1 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -151,9 +151,7 @@ def __init__(self, self._learner['ml_g'] = ml_g # Question: Add a warning when ml_g is set for partialling out score where it is not required / used? 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) + 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' From 9a513f0a0df0b81606a2a5e8f3f753ca800e1bf0 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 10:37:07 +0200 Subject: [PATCH 38/47] add an additional exception handling unit test for pliv IV-type with learner ml_g=None --- doubleml/tests/test_doubleml_exceptions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index ef333ee8..78e3e84d 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -492,6 +492,11 @@ def test_doubleml_exception_learner(): with pytest.warns(DeprecationWarning, match=msg): _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) + 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') + # 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.') From 3ce617fffb8dcc2f911b03cbc0de1de0b6254a17 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 11:37:47 +0200 Subject: [PATCH 39/47] pep8 fixes --- doubleml/double_ml_pliv.py | 3 +-- doubleml/tests/test_doubleml_scores.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index 5ebbe1f1..fe19f947 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -4,7 +4,6 @@ from sklearn.model_selection import GridSearchCV, RandomizedSearchCV from sklearn.linear_model import LinearRegression from sklearn.dummy import DummyRegressor -from sklearn.base import clone import warnings from functools import wraps @@ -520,7 +519,7 @@ def tune(self, scoring_methods['ml_l'] = scoring_methods.pop('ml_g') super(DoubleMLPLIV, self).tune(param_grids, tune_on_folds, scoring_methods, n_folds_tune, search_mode, - n_iter_randomized_search, n_jobs_cv, set_as_params, return_tune_res) + n_iter_randomized_search, n_jobs_cv, set_as_params, return_tune_res) def _nuisance_tuning_partial_x(self, smpls, param_grids, scoring_methods, n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search): diff --git a/doubleml/tests/test_doubleml_scores.py b/doubleml/tests/test_doubleml_scores.py index 36e4194b..28007331 100644 --- a/doubleml/tests/test_doubleml_scores.py +++ b/doubleml/tests/test_doubleml_scores.py @@ -60,7 +60,7 @@ def non_orth_score_w_l(y, d, l_hat, m_hat, g_hat, smpls): p_a = -np.multiply(d - m_hat, d - m_hat) p_b = np.multiply(d - m_hat, y - l_hat) theta_initial = -np.nanmean(p_b) / np.nanmean(p_a) - g_hat = l_hat - np.multiply(d, m_hat) + g_hat = l_hat - theta_initial * np.multiply(d, m_hat) u_hat = y - g_hat psi_a = -np.multiply(d, d) From 4cc0320cebb6ce68fc3b08bb0fb2dc3ef835bda0 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 11:47:29 +0200 Subject: [PATCH 40/47] add ignore for pylint in exception handling unit test --- doubleml/tests/test_doubleml_exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 78e3e84d..f0d19a7c 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -486,11 +486,11 @@ def test_doubleml_exception_learner(): msg = 'ml_g was renamed to ml_l' with pytest.warns(DeprecationWarning, match=msg): - _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) + _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) # pylint: no-value-for-parameter msg = 'ml_g was renamed to ml_l' with pytest.warns(DeprecationWarning, match=msg): - _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) + _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) # pylint: no-value-for-parameter 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): From 76aeea8bae8b0955e63d56bde65fbfafdfd15fc3 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 11:54:11 +0200 Subject: [PATCH 41/47] add ignore for pylint in exception handling unit test --- doubleml/tests/test_doubleml_exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index f0d19a7c..2f4805ac 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -486,11 +486,11 @@ def test_doubleml_exception_learner(): msg = 'ml_g was renamed to ml_l' with pytest.warns(DeprecationWarning, match=msg): - _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) # pylint: no-value-for-parameter + _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) # pylint: disable=no-value-for-parameter msg = 'ml_g was renamed to ml_l' with pytest.warns(DeprecationWarning, match=msg): - _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) # pylint: no-value-for-parameter + _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) # pylint: disable=no-value-for-parameter 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): From efcdd131363410ae1411ef01af501da56adc25b9 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 12:37:12 +0200 Subject: [PATCH 42/47] with IV-type score, three learners should be specified --- doubleml/tests/test_doubleml_scores.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doubleml/tests/test_doubleml_scores.py b/doubleml/tests/test_doubleml_scores.py index 28007331..8d0a04fb 100644 --- a/doubleml/tests/test_doubleml_scores.py +++ b/doubleml/tests/test_doubleml_scores.py @@ -14,7 +14,7 @@ dml_plr = DoubleMLPLR(dml_data_plr, Lasso(), Lasso()) dml_plr.fit() -dml_plr_iv_type = DoubleMLPLR(dml_data_plr, 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() From bc7c2c77aa6936a086c98f687a72fa8d0a2c15f2 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 12:48:29 +0200 Subject: [PATCH 43/47] added another exception handling / warnings unit test --- doubleml/tests/test_doubleml_exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 2f4805ac..0da9f7e6 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -488,6 +488,10 @@ def test_doubleml_exception_learner(): with pytest.warns(DeprecationWarning, match=msg): _ = DoubleMLPLR(dml_data, ml_g=Lasso(), ml_m=ml_m) # pylint: disable=no-value-for-parameter + 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') + msg = 'ml_g was renamed to ml_l' with pytest.warns(DeprecationWarning, match=msg): _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) # pylint: disable=no-value-for-parameter From 7dcbceb5b37dbae823246b062adc169b961b9719 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 20 May 2022 14:35:06 +0200 Subject: [PATCH 44/47] tune g only if it is necessary --- doubleml/tests/test_pliv_tune.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doubleml/tests/test_pliv_tune.py b/doubleml/tests/test_pliv_tune.py index efb70862..a7ff7959 100644 --- a/doubleml/tests/test_pliv_tune.py +++ b/doubleml/tests/test_pliv_tune.py @@ -108,19 +108,22 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_ all_smpls = draw_smpls(n_obs, n_folds) smpls = all_smpls[0] + 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']) + 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']) + 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 From db74e05cebd692121b81ccacacada6528d44edfc Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 10 Jun 2022 08:32:43 +0200 Subject: [PATCH 45/47] add a warning if a learner ml_g is specified (but not needed) with score partialling out --- doubleml/double_ml_pliv.py | 7 +++++-- doubleml/double_ml_plr.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doubleml/double_ml_pliv.py b/doubleml/double_ml_pliv.py index fe19f947..8d66d240 100644 --- a/doubleml/double_ml_pliv.py +++ b/doubleml/double_ml_pliv.py @@ -145,10 +145,13 @@ def __init__(self, _ = 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: - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) 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 - # Question: Add a warning when ml_g is set for partialling out score where it is not required / used? + 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'): 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'} diff --git a/doubleml/double_ml_plr.py b/doubleml/double_ml_plr.py index e54ac4cb..5d25600e 100644 --- a/doubleml/double_ml_plr.py +++ b/doubleml/double_ml_plr.py @@ -136,10 +136,13 @@ def __init__(self, self._learner = {'ml_l': ml_l, 'ml_m': ml_m} if ml_g is not None: - _ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False) 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 - # Question: Add a warning when ml_g is set for partialling out score where it is not required / used? + 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).")) From 8eb4de7e5d260b2cd5b228f2fd87a2d51f7a0a73 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 10 Jun 2022 08:55:22 +0200 Subject: [PATCH 46/47] fix unit tests after the newly introduced warning if ml_g is specified with score ml_g (db74e05cebd692121b81ccacacada6528d44edfc) --- doubleml/tests/_utils.py | 9 ++++ doubleml/tests/test_multiway_cluster.py | 41 ++++++++++-------- doubleml/tests/test_pliv_no_cross_fit.py | 17 ++++---- doubleml/tests/test_pliv_tune.py | 23 +++++----- doubleml/tests/test_plr_classifier.py | 15 ++++--- doubleml/tests/test_plr_multi_treat.py | 17 ++++---- doubleml/tests/test_plr_no_cross_fit.py | 43 +++++++++++-------- .../tests/test_plr_reestimate_from_scores.py | 14 +++--- doubleml/tests/test_plr_rep_cross.py | 17 ++++---- .../tests/test_plr_set_ml_nuisance_pars.py | 24 +++++++---- .../tests/test_plr_set_smpls_externally.py | 14 +++--- doubleml/tests/test_plr_tune.py | 19 ++++---- 12 files changed, 148 insertions(+), 105 deletions(-) diff --git a/doubleml/tests/_utils.py b/doubleml/tests/_utils.py index 3f318701..d9152bcb 100644 --- a/doubleml/tests/_utils.py +++ b/doubleml/tests/_utils.py @@ -1,5 +1,6 @@ import numpy as np from sklearn.model_selection import KFold, GridSearchCV +from sklearn.base import clone def draw_smpls(n_obs, n_folds, n_rep=1): @@ -58,3 +59,11 @@ def tune_grid_search(y, x, ml_model, smpls, param_grid, n_folds_tune, train_cond tune_res[idx] = g_grid_search.fit(x[train_index_cond, :], y[train_index_cond]) return tune_res + + +def _clone(learner): + if learner is None: + res = None + else: + res = clone(learner) + return res diff --git a/doubleml/tests/test_multiway_cluster.py b/doubleml/tests/test_multiway_cluster.py index ee677210..73b66b27 100644 --- a/doubleml/tests/test_multiway_cluster.py +++ b/doubleml/tests/test_multiway_cluster.py @@ -2,14 +2,13 @@ import pytest import math -from sklearn.base import clone - from sklearn.linear_model import LinearRegression, Lasso from sklearn.ensemble import RandomForestRegressor import doubleml as dml from doubleml.datasets import make_pliv_multiway_cluster_CKMS2021 +from ._utils import _clone from ._utils_cluster import DoubleMLMultiwayResampling, var_one_way_cluster, est_one_way_cluster_dml2,\ est_two_way_cluster_dml2, var_two_way_cluster from ._utils_pliv_manual import fit_pliv, compute_pliv_residuals @@ -62,9 +61,9 @@ def dml_pliv_multiway_cluster_old_vs_new_fixture(generate_data_iv, learner): _, smpls_lin_ind = obj_dml_multiway_resampling.split_samples() # Set machine learning methods for l, m & r - ml_l = clone(learner) - ml_m = clone(learner) - ml_r = clone(learner) + ml_l = _clone(learner) + ml_m = _clone(learner) + ml_r = _clone(learner) df = obj_dml_cluster_data.data.set_index(['cluster_var_i', 'cluster_var_j']) obj_dml_data = dml.DoubleMLData(df, @@ -108,10 +107,13 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score, dml_proc n_rep = 2 # Set machine learning methods for l, m, r & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_r = clone(learner) - ml_g = clone(learner) + ml_l = _clone(learner) + ml_m = _clone(learner) + ml_r = _clone(learner) + 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, @@ -131,7 +133,7 @@ def dml_pliv_multiway_cluster_fixture(generate_data_iv, learner, score, dml_proc z = np.ravel(obj_dml_cluster_data.z) res_manual = fit_pliv(y, x, d, z, - clone(learner), clone(learner), clone(learner), clone(learner), + _clone(learner), _clone(learner), _clone(learner), _clone(learner), dml_pliv_obj.smpls, dml_procedure, score, n_rep=n_rep) thetas = np.full(n_rep, np.nan) @@ -209,11 +211,14 @@ def test_dml_pliv_multiway_cluster_se(dml_pliv_multiway_cluster_fixture): def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, score, dml_procedure): n_folds = 3 - # Set machine learning methods for l, m & r - ml_l = clone(learner) - ml_m = clone(learner) - ml_r = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m, r & g + ml_l = _clone(learner) + ml_m = _clone(learner) + ml_r = _clone(learner) + 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, @@ -232,7 +237,7 @@ def dml_pliv_oneway_cluster_fixture(generate_data_iv, learner, score, dml_proced 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), + _clone(learner), _clone(learner), _clone(learner), _clone(learner), dml_pliv_obj.smpls, dml_procedure, score) l_hat = res_manual['all_l_hat'][0] m_hat = res_manual['all_m_hat'][0] @@ -301,8 +306,8 @@ def dml_plr_cluster_with_index(generate_data1, learner, dml_procedure): 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) + ml_l = _clone(learner) + ml_m = _clone(learner) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) np.random.seed(3141) diff --git a/doubleml/tests/test_pliv_no_cross_fit.py b/doubleml/tests/test_pliv_no_cross_fit.py index 35c03053..f014009b 100644 --- a/doubleml/tests/test_pliv_no_cross_fit.py +++ b/doubleml/tests/test_pliv_no_cross_fit.py @@ -2,13 +2,11 @@ import pytest import math -from sklearn.base import clone - from sklearn.ensemble import RandomForestRegressor import doubleml as dml -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_pliv_manual import fit_pliv, boot_pliv @@ -41,10 +39,13 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): x_cols = data.columns[data.columns.str.startswith('X')].tolist() # Set machine learning methods for l, m, r & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_r = clone(learner) - ml_g = clone(learner) + ml_l = _clone(learner) + ml_m = _clone(learner) + ml_r = _clone(learner) + 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, 'Z1') @@ -71,7 +72,7 @@ def dml_pliv_no_cross_fit_fixture(generate_data_iv, learner, score, n_folds): smpls = [smpls[0]] res_manual = fit_pliv(y, x, d, z, - clone(learner), clone(learner), clone(learner), clone(learner), + _clone(learner), _clone(learner), _clone(learner), _clone(learner), [smpls], dml_procedure, score) res_dict = {'coef': dml_pliv_obj.coef, diff --git a/doubleml/tests/test_pliv_tune.py b/doubleml/tests/test_pliv_tune.py index a7ff7959..40252bb7 100644 --- a/doubleml/tests/test_pliv_tune.py +++ b/doubleml/tests/test_pliv_tune.py @@ -2,13 +2,11 @@ import pytest import math -from sklearn.base import clone - from sklearn.linear_model import Lasso, ElasticNet import doubleml as dml -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_pliv_manual import fit_pliv, boot_pliv, tune_nuisance_pliv @@ -80,11 +78,14 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_ data = generate_data_iv x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for l, m & r - ml_l = clone(learner_l) - ml_m = clone(learner_m) - ml_r = clone(learner_r) - ml_g = clone(learner_g) + # 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': + ml_g = _clone(learner_g) + else: + ml_g = None np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols, 'Z1') @@ -112,7 +113,7 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_ 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), + _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) @@ -120,7 +121,7 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_ 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), + _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) @@ -130,7 +131,7 @@ def dml_pliv_fixture(generate_data_iv, learner_l, learner_m, learner_r, learner_ 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), + res_manual = fit_pliv(y, x, d, z, _clone(learner_l), _clone(learner_m), _clone(learner_r), _clone(learner_g), all_smpls, dml_procedure, score, l_params=l_params, m_params=m_params, r_params=r_params, g_params=g_params) diff --git a/doubleml/tests/test_plr_classifier.py b/doubleml/tests/test_plr_classifier.py index c40e3ffe..6be28cff 100644 --- a/doubleml/tests/test_plr_classifier.py +++ b/doubleml/tests/test_plr_classifier.py @@ -2,15 +2,13 @@ import pytest import math -from sklearn.base import clone - from sklearn.linear_model import Lasso, LogisticRegression from sklearn.ensemble import RandomForestClassifier import doubleml as dml from doubleml.datasets import fetch_bonus -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_plr_manual import fit_plr, boot_plr bonus_data = fetch_bonus() @@ -42,10 +40,13 @@ def dml_plr_binary_classifier_fixture(learner, score, dml_procedure): n_folds = 2 n_rep_boot = 502 - # Set machine learning methods for m & g + # Set machine learning methods for l, m & g ml_l = Lasso(alpha=0.3) - ml_g = Lasso() - ml_m = clone(learner) + ml_m = _clone(learner) + if score == 'IV-type': + ml_g = Lasso() + else: + ml_g = None np.random.seed(3141) dml_plr_obj = dml.DoubleMLPLR(bonus_data, @@ -63,7 +64,7 @@ def dml_plr_binary_classifier_fixture(learner, score, dml_procedure): 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), + res_manual = fit_plr(y, x, d, _clone(ml_l), _clone(ml_m), _clone(ml_g), all_smpls, dml_procedure, score) res_dict = {'coef': dml_plr_obj.coef, diff --git a/doubleml/tests/test_plr_multi_treat.py b/doubleml/tests/test_plr_multi_treat.py index bda58d2b..f3077828 100644 --- a/doubleml/tests/test_plr_multi_treat.py +++ b/doubleml/tests/test_plr_multi_treat.py @@ -1,14 +1,12 @@ import numpy as np import pytest -from sklearn.base import clone - from sklearn.linear_model import Lasso from sklearn.ensemble import RandomForestRegressor import doubleml as dml -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_plr_manual import fit_plr_multitreat, boot_plr_multitreat @@ -59,10 +57,13 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, 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 = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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) @@ -82,7 +83,7 @@ def dml_plr_multitreat_fixture(generate_data_bivariate, generate_data_toeplitz, all_smpls = draw_smpls(n_obs, n_folds, n_rep) res_manual = fit_plr_multitreat(y, x, d, - clone(learner), clone(learner), clone(learner), + _clone(learner), _clone(learner), _clone(learner), all_smpls, dml_procedure, score, n_rep=n_rep) diff --git a/doubleml/tests/test_plr_no_cross_fit.py b/doubleml/tests/test_plr_no_cross_fit.py index c0ef58a9..761b19bc 100644 --- a/doubleml/tests/test_plr_no_cross_fit.py +++ b/doubleml/tests/test_plr_no_cross_fit.py @@ -2,13 +2,11 @@ import pytest import math -from sklearn.base import clone - from sklearn.linear_model import Lasso import doubleml as dml -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_plr_manual import fit_plr, plr_dml1, fit_nuisance_plr, boot_plr, tune_nuisance_plr @@ -40,10 +38,13 @@ def dml_plr_no_cross_fit_fixture(generate_data1, learner, score, n_folds): data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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) @@ -68,7 +69,7 @@ def dml_plr_no_cross_fit_fixture(generate_data1, learner, score, n_folds): smpls = all_smpls[0] smpls = [smpls[0]] - res_manual = fit_plr(y, x, d, clone(learner), clone(learner), clone(learner), + res_manual = fit_plr(y, x, d, _clone(learner), _clone(learner), _clone(learner), [smpls], dml_procedure, score) res_dict = {'coef': dml_plr_obj.coef, @@ -133,10 +134,13 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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) @@ -169,7 +173,7 @@ def dml_plr_rep_no_cross_fit_fixture(generate_data1, learner, score, n_rep): smpls = all_smpls[i_rep] l_hat, m_hat, g_hat = fit_nuisance_plr(y, x, d, - clone(learner), clone(learner), clone(learner), smpls) + _clone(learner), _clone(learner), _clone(learner), smpls) all_l_hat.append(l_hat) all_m_hat.append(m_hat) @@ -253,10 +257,13 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g + # Set machine learning methods for l, m & g ml_l = Lasso() ml_m = Lasso() - ml_g = Lasso() + if score == 'IV-type': + ml_g = Lasso() + else: + ml_g = None np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d'], x_cols) @@ -288,19 +295,19 @@ def dml_plr_no_cross_fit_tune_fixture(generate_data1, learner, score, tune_on_fo par_grid['ml_g'] = None if tune_on_folds: l_params, m_params, g_params = tune_nuisance_plr(y, x, d, - clone(ml_l), clone(ml_m), clone(ml_g), + _clone(ml_l), _clone(ml_m), _clone(ml_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(ml_l), clone(ml_m), clone(ml_g), + _clone(ml_l), _clone(ml_m), _clone(ml_g), xx, n_folds_tune, par_grid['ml_l'], par_grid['ml_m'], par_grid['ml_g'], tune_g) - res_manual = fit_plr(y, x, d, clone(ml_l), clone(ml_m), clone(ml_g), + res_manual = fit_plr(y, x, d, _clone(ml_l), _clone(ml_m), _clone(ml_g), [smpls], dml_procedure, score, l_params=l_params, m_params=m_params, g_params=g_params) diff --git a/doubleml/tests/test_plr_reestimate_from_scores.py b/doubleml/tests/test_plr_reestimate_from_scores.py index 80fcad37..7d5d732a 100644 --- a/doubleml/tests/test_plr_reestimate_from_scores.py +++ b/doubleml/tests/test_plr_reestimate_from_scores.py @@ -2,11 +2,12 @@ import pytest import math -from sklearn.base import clone from sklearn.linear_model import LinearRegression import doubleml as dml +from ._utils import _clone + @pytest.fixture(scope='module', params=[LinearRegression()]) @@ -40,10 +41,13 @@ def dml_plr_reestimate_fixture(generate_data1, learner, score, dml_procedure, n_ data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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) diff --git a/doubleml/tests/test_plr_rep_cross.py b/doubleml/tests/test_plr_rep_cross.py index 1650dbf2..f2a50e21 100644 --- a/doubleml/tests/test_plr_rep_cross.py +++ b/doubleml/tests/test_plr_rep_cross.py @@ -2,14 +2,12 @@ import pytest import math -from sklearn.base import clone - from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor import doubleml as dml -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_plr_manual import fit_plr, boot_plr @@ -48,10 +46,13 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure, n_rep): data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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) @@ -71,7 +72,7 @@ def dml_plr_fixture(generate_data1, learner, score, dml_procedure, n_rep): 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), + res_manual = fit_plr(y, x, d, _clone(learner), _clone(learner), _clone(learner), all_smpls, dml_procedure, score, n_rep) res_dict = {'coef': dml_plr_obj.coef, diff --git a/doubleml/tests/test_plr_set_ml_nuisance_pars.py b/doubleml/tests/test_plr_set_ml_nuisance_pars.py index c80e6c2a..d8b99840 100644 --- a/doubleml/tests/test_plr_set_ml_nuisance_pars.py +++ b/doubleml/tests/test_plr_set_ml_nuisance_pars.py @@ -2,11 +2,12 @@ import pytest import math -from sklearn.base import clone from sklearn.linear_model import Lasso import doubleml as dml +from ._utils import _clone + @pytest.fixture(scope='module', params=['IV-type', 'partialling out']) @@ -31,10 +32,13 @@ def dml_plr_fixture(generate_data1, score, dml_procedure): alpha = 0.05 learner = Lasso(alpha=alpha) - # Set machine learning methods for m & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + if score == 'IV-type': + ml_g = _clone(learner) + else: + ml_g = None np.random.seed(3141) obj_dml_data = dml.DoubleMLData(data, 'y', ['d']) @@ -48,9 +52,13 @@ def dml_plr_fixture(generate_data1, score, dml_procedure): np.random.seed(3141) learner = Lasso() - # Set machine learning methods for m & g - ml_g = clone(learner) - ml_m = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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, diff --git a/doubleml/tests/test_plr_set_smpls_externally.py b/doubleml/tests/test_plr_set_smpls_externally.py index c236d8a9..e6268217 100644 --- a/doubleml/tests/test_plr_set_smpls_externally.py +++ b/doubleml/tests/test_plr_set_smpls_externally.py @@ -2,11 +2,12 @@ import pytest import math -from sklearn.base import clone from sklearn.linear_model import LinearRegression import doubleml as dml +from ._utils import _clone + @pytest.fixture(scope='module', params=[LinearRegression()]) @@ -40,10 +41,13 @@ def dml_plr_smpls_fixture(generate_data1, learner, score, dml_procedure, n_rep): data = generate_data1 x_cols = data.columns[data.columns.str.startswith('X')].tolist() - # Set machine learning methods for m & g - ml_l = clone(learner) - ml_m = clone(learner) - ml_g = clone(learner) + # Set machine learning methods for l, m & g + ml_l = _clone(learner) + ml_m = _clone(learner) + 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) diff --git a/doubleml/tests/test_plr_tune.py b/doubleml/tests/test_plr_tune.py index 1af5f0b3..c18c470e 100644 --- a/doubleml/tests/test_plr_tune.py +++ b/doubleml/tests/test_plr_tune.py @@ -2,13 +2,11 @@ import pytest import math -from sklearn.base import clone - from sklearn.linear_model import Lasso, ElasticNet import doubleml as dml -from ._utils import draw_smpls +from ._utils import draw_smpls, _clone from ._utils_plr_manual import fit_plr, boot_plr, tune_nuisance_plr @@ -75,9 +73,12 @@ def dml_plr_fixture(generate_data2, learner_l, learner_m, learner_g, score, dml_ obj_dml_data = generate_data2 # Set machine learning methods for m & g - ml_l = clone(learner_l) - ml_m = clone(learner_m) - ml_g = clone(learner_g) + ml_l = _clone(learner_l) + ml_m = _clone(learner_m) + 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, @@ -103,20 +104,20 @@ def dml_plr_fixture(generate_data2, learner_l, learner_m, learner_g, score, dml_ 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), + _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), + _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), + res_manual = fit_plr(y, x, d, _clone(learner_l), _clone(learner_m), _clone(learner_g), all_smpls, dml_procedure, score, l_params=l_params, m_params=m_params, g_params=g_params) From 8875602b31f3494c5312f06830751b6b7f8a26c2 Mon Sep 17 00:00:00 2001 From: "Malte S. Kurz" Date: Fri, 10 Jun 2022 12:03:27 +0200 Subject: [PATCH 47/47] added a unit test for the new warning (see db74e05cebd692121b81ccacacada6528d44edfc) --- doubleml/tests/test_doubleml_exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doubleml/tests/test_doubleml_exceptions.py b/doubleml/tests/test_doubleml_exceptions.py index 0da9f7e6..875902d1 100644 --- a/doubleml/tests/test_doubleml_exceptions.py +++ b/doubleml/tests/test_doubleml_exceptions.py @@ -492,6 +492,10 @@ def test_doubleml_exception_learner(): with pytest.warns(UserWarning, match=msg): _ = 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') + msg = 'ml_g was renamed to ml_l' with pytest.warns(DeprecationWarning, match=msg): _ = DoubleMLPLIV(dml_data_pliv, ml_g=Lasso(), ml_m=ml_m, ml_r=ml_r) # pylint: disable=no-value-for-parameter @@ -501,6 +505,10 @@ def test_doubleml_exception_learner(): _ = 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') + # 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.')