Skip to content

Adapt nuisance est for IV-type score (PLR) & new score IV-type for PLIV #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 55 commits into from
Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c86b332
adapt the nuisance estimation for the IV type score in the PLR model
MalteKurz Feb 10, 2022
174ebad
some fixes for the adaption of nuisance learning for the IV-type scor…
MalteKurz Mar 9, 2022
dd9ae4c
align unit tests with the adapted implementation of nuisance learning…
MalteKurz Mar 9, 2022
64866df
refactor the functional PLR implementation
MalteKurz Apr 21, 2022
d99ff13
update the documentation for the PLR model
MalteKurz Apr 21, 2022
f5287e6
fix documentation and error message
MalteKurz Apr 22, 2022
acc4da6
fix unit tests after refactoring in 64866dfcef988ac205755b07347df89f5…
MalteKurz Apr 22, 2022
f562087
fix unit tests after refactoring in 64866dfcef988ac205755b07347df89f5…
MalteKurz Apr 22, 2022
a3f56f1
Merge branch 'master' of github.com:DoubleML/doubleml-for-py into m-i…
MalteKurz Apr 22, 2022
bb870cc
renamed ml_g to ml_l; Add additional learner ml_g for IV-type score
MalteKurz Apr 29, 2022
4e6ecaf
update documentation
MalteKurz Apr 29, 2022
81edf45
simplify code to set learner and predict_method
MalteKurz Apr 29, 2022
405dc58
additional changes for the new API with ml_g and ml_l
MalteKurz Apr 29, 2022
93ad699
fix deprecation warning and add corresponding unit tests
MalteKurz Apr 29, 2022
43a74bb
change order of predictions to be consistently l_hat, m_hat, g_hat
MalteKurz Apr 29, 2022
27980cf
add unit test with callable for IV-type score
MalteKurz Apr 29, 2022
16a90d4
increase number of observations in dummy test cases
MalteKurz Apr 29, 2022
a7b6972
added some additional unit tests for non-orth scores via callable scores
MalteKurz May 2, 2022
7d0e0b7
renamed ml_g into ml_l
MalteKurz May 2, 2022
871b762
also rename g_hat into l_hat; start implementing the IV-type score
MalteKurz May 2, 2022
b93e0e8
renaming of ml_g to ml_l also in the unit tests
MalteKurz May 2, 2022
f504590
also align names in the tuning parts with the renamed learner ml_g to…
MalteKurz May 2, 2022
22a2a72
adapt unit test after renaming ml_g to ml_l
MalteKurz May 2, 2022
80a0c6e
implementation of the IV-type score for the PLIV model
MalteKurz May 2, 2022
ff944df
remove assert; score could also be a callable
MalteKurz May 2, 2022
dc0ef90
a couple more renamings ml_g to ml_l for consistency
MalteKurz May 2, 2022
c718590
Merge branch 'm-plr-api' of github.com:DoubleML/doubleml-for-py into …
MalteKurz May 2, 2022
58a4674
refactor the check and set learner part in the initializer
MalteKurz May 3, 2022
1dadf61
pass n_folds as keyword argument to fix unit tests
MalteKurz May 3, 2022
a563dd0
Merge branch 'm-plr-api' of github.com:DoubleML/doubleml-for-py into …
MalteKurz May 3, 2022
cbf3e02
refactor the check and set learner part of the initializer
MalteKurz May 3, 2022
5b3bca8
finalize the implementation of the IV-type score for PLIV: Manual imp…
MalteKurz May 3, 2022
4b6f9a0
add IV-type score also to the unit tests for the multiway clustering …
MalteKurz May 3, 2022
2ddabc3
add unit tests for the deprecation warnings for the PLIV model; ml_g …
MalteKurz May 3, 2022
bce9aad
fix notation in docu
MalteKurz May 5, 2022
981acaa
fix notation in docu
MalteKurz May 5, 2022
631c2c4
fix notation in docu
MalteKurz May 5, 2022
577154f
fix notation in docu
MalteKurz May 5, 2022
1fca759
provide all arguments as keyword arguments to callable scores
MalteKurz May 5, 2022
7067cf7
Merge branch 'master' of github.com:DoubleML/doubleml-for-py into m-i…
MalteKurz May 13, 2022
ae90022
Merge branch 'm-iv-type-score' of github.com:DoubleML/doubleml-for-py…
MalteKurz May 13, 2022
9aecbae
Merge branch 'm-plr-api' of github.com:DoubleML/doubleml-for-py into …
MalteKurz May 13, 2022
51d64c1
docu update
MalteKurz May 20, 2022
bfdb9fc
IV type score is new for PLIV; no need to warn; instead throw an exce…
MalteKurz May 20, 2022
9a513f0
add an additional exception handling unit test for pliv IV-type with …
MalteKurz May 20, 2022
3ce617f
pep8 fixes
MalteKurz May 20, 2022
4cc0320
add ignore for pylint in exception handling unit test
MalteKurz May 20, 2022
76aeea8
add ignore for pylint in exception handling unit test
MalteKurz May 20, 2022
efcdd13
with IV-type score, three learners should be specified
MalteKurz May 20, 2022
bc7c2c7
added another exception handling / warnings unit test
MalteKurz May 20, 2022
7dcbceb
tune g only if it is necessary
MalteKurz May 20, 2022
db74e05
add a warning if a learner ml_g is specified (but not needed) with sc…
MalteKurz Jun 10, 2022
8eb4de7
fix unit tests after the newly introduced warning if ml_g is specifie…
MalteKurz Jun 10, 2022
8875602
added a unit test for the new warning (see db74e05cebd692121b81ccacac…
MalteKurz Jun 10, 2022
4346b23
Merge branch 'master' of github.com:DoubleML/doubleml-for-py into m-p…
MalteKurz Jun 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doubleml/double_ml_iivm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion doubleml/double_ml_irm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
291 changes: 211 additions & 80 deletions doubleml/double_ml_pliv.py

Large diffs are not rendered by default.

196 changes: 163 additions & 33 deletions doubleml/double_ml_plr.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
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


# 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'})) + 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

Expand All @@ -14,9 +34,9 @@ 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:`g_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.
Expand All @@ -25,6 +45,13 @@ 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]`.
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``.
Expand All @@ -35,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, 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
Expand Down Expand Up @@ -81,10 +108,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',
Expand All @@ -101,22 +130,41 @@ 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)

_ = 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_g': ml_g, 'ml_m': ml_m}
self._learner = {'ml_l': ml_l, 'ml_m': ml_m}

if ml_g is not None:
if (isinstance(self.score, str) & (self.score == 'IV-type')) | callable(self.score):
_ = self._check_learner(ml_g, 'ml_g', regressor=True, classifier=False)
self._learner['ml_g'] = ml_g
else:
assert (isinstance(self.score, str) & (self.score == 'partialling out'))
warnings.warn(('A learner ml_g has been provided for score = "partialling out" but will be ignored. "'
'A learner ml_g is not required for estimation.'))
elif isinstance(self.score, str) & (self.score == 'IV-type'):
warnings.warn(("For score = 'IV-type', learners ml_l and ml_g should be specified. "
"Set ml_g = clone(ml_l)."))
self._learner['ml_g'] = clone(ml_l)

self._predict_method = {'ml_l': 'predict'}
if 'ml_g' in self._learner:
self._predict_method['ml_g'] = 'predict'
if ml_m_is_classifier:
if obj_dml_data.binary_treats.all():
self._predict_method = {'ml_g': 'predict', 'ml_m': 'predict_proba'}
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_g': 'predict', 'ml_m': 'predict'}
self._predict_method['ml_m'] = 'predict'

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 self._learner}

def _check_score(self, score):
if isinstance(score, str):
Expand All @@ -138,16 +186,27 @@ def _check_data(self, obj_dml_data):
'To fit a partially linear IV regression model use DoubleMLPLIV instead of DoubleMLPLR.')
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(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)
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,
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)
# nuisance l
l_hat = _dml_cv_predict(self._learner['ml_l'], x, y, smpls=smpls, n_jobs=n_jobs_cv,
est_params=self._get_params('ml_l'), method=self._predict_method['ml_l'])
_check_finite_predictions(l_hat, 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,
Expand All @@ -163,31 +222,79 @@ 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)
preds = {'ml_g': g_hat,
'ml_m': m_hat}
# an estimate of g is obtained for the IV-type score and callable scores
g_hat = None
if 'ml_g' in self._learner:
# get an initial estimate for theta using the partialling out score
psi_a = -np.multiply(d - m_hat, d - m_hat)
psi_b = np.multiply(d - 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, 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, g_hat, m_hat, smpls):
def _score_elements(self, y, d, l_hat, m_hat, g_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=y, d=d,
l_hat=l_hat, m_hat=m_hat, g_hat=g_hat,
smpls=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,
Expand All @@ -196,25 +303,48 @@ 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]
g_tune_res = _dml_tune(y, x, train_inds,
self._learner['ml_g'], param_grids['ml_g'], scoring_methods['ml_g'],
l_tune_res = _dml_tune(y, x, train_inds,
self._learner['ml_l'], param_grids['ml_l'], scoring_methods['ml_l'],
n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search)
m_tune_res = _dml_tune(d, x, train_inds,
self._learner['ml_m'], param_grids['ml_m'], scoring_methods['ml_m'],
n_folds_tune, n_jobs_cv, search_mode, n_iter_randomized_search)

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 '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)
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 = _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:
params = {'ml_l': l_best_params,
'ml_m': m_best_params}
tune_res = {'l_tune': l_tune_res,
'm_tune': m_tune_res}

res = {'params': params,
'tune_res': tune_res}
Expand Down
9 changes: 9 additions & 0 deletions doubleml/tests/_utils.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
Loading