Skip to content

Commit

Permalink
Merge branch 'parametrize_evaluate' into implement_grid_search
Browse files Browse the repository at this point in the history
  • Loading branch information
FrancescMartiEscofetQC committed Jun 24, 2024
2 parents 9789e90 + ecfd745 commit 6771e5a
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 406 deletions.
634 changes: 310 additions & 324 deletions docs/examples/example_optuna.ipynb

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions metalearners/_typing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# # Copyright (c) QuantCo 2024-2024
# # SPDX-License-Identifier: BSD-3-Clause

from collections.abc import Collection, Mapping
from collections.abc import Callable, Collection, Mapping, Sequence
from typing import Literal, Protocol, Union

import numpy as np
Expand Down Expand Up @@ -29,7 +29,6 @@ class _ScikitModel(Protocol):

# https://stackoverflow.com/questions/54868698/what-type-is-a-sklearn-model/60542986#60542986
def fit(self, X, y, *params, **kwargs): ...

def predict(self, X, *params, **kwargs): ...

def score(self, X, y, **kwargs): ...
Expand All @@ -44,3 +43,7 @@ def set_params(self, **params): ...
# For instance, if converting the Generator resulting from a call to
# sklearn.model_selection.KFold.split to a list we obtain this type.
SplitIndices = list[tuple[np.ndarray, np.ndarray]]

Scorer = str | Callable
Scorers = Sequence[Scorer]
Scoring = Mapping[str, Scorers]
8 changes: 7 additions & 1 deletion metalearners/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def simplify_output_2d(tensor: np.ndarray) -> np.ndarray:
# Taken from https://stackoverflow.com/questions/13741998/is-there-a-way-to-let-classes-inherit-the-documentation-of-their-superclass-with
def copydoc(fromfunc, sep="\n"):
"""
Decorator: Copy the docstring of `fromfunc`
Decorator: Copy the docstring of ``fromfunc``
"""

def _decorator(func):
Expand All @@ -489,3 +489,9 @@ def _decorator(func):
return func

return _decorator


def default_metric(predict_method: PredictMethod) -> str:
if predict_method == _PREDICT_PROBA:
return "neg_log_loss"
return "neg_root_mean_squared_error"
26 changes: 9 additions & 17 deletions metalearners/drlearner.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# # Copyright (c) QuantCo 2024-2024
# # SPDX-License-Identifier: BSD-3-Clause

from collections.abc import Callable, Mapping

import numpy as np
from joblib import Parallel, delayed
from typing_extensions import Self

from metalearners._typing import Matrix, OosMethod, Vector
from metalearners._typing import Matrix, OosMethod, Scoring, Vector
from metalearners._utils import (
clip_element_absolute_value_to_epsilon,
get_one,
Expand Down Expand Up @@ -209,37 +208,30 @@ def evaluate(
w: Vector,
is_oos: bool,
oos_method: OosMethod = OVERALL,
scoring: Mapping[str, list[str | Callable]] | None = None,
scoring: Scoring | None = None,
) -> dict[str, float]:
if scoring is None:
scoring = {}
safe_scoring = self._scoring(scoring)

default_metric = (
"neg_log_loss" if self.is_classification else "neg_root_mean_squared_error"
)
masks = []
for tv in range(self.n_variants):
masks.append(w == tv)
variant_outcome_evaluation = _evaluate_model_kind(
cfes=self._nuisance_models[VARIANT_OUTCOME_MODEL],
Xs=[X[w == tv] for tv in range(self.n_variants)],
ys=[y[w == tv] for tv in range(self.n_variants)],
scorers=scoring.get(VARIANT_OUTCOME_MODEL, [default_metric]),
scorers=safe_scoring[VARIANT_OUTCOME_MODEL],
model_kind=VARIANT_OUTCOME_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=False,
is_treatment_model=False,
)

propensity_evaluation = _evaluate_model_kind(
cfes=self._nuisance_models[PROPENSITY_MODEL],
Xs=[X],
ys=[w],
scorers=scoring.get(PROPENSITY_MODEL, ["neg_log_loss"]),
scorers=safe_scoring[PROPENSITY_MODEL],
model_kind=PROPENSITY_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=False,
is_treatment_model=False,
)

pseudo_outcome: list[np.ndarray] = []
Expand All @@ -258,11 +250,11 @@ def evaluate(
self._treatment_models[TREATMENT_MODEL],
Xs=[X for _ in range(1, self.n_variants)],
ys=pseudo_outcome,
scorers=scoring.get(TREATMENT_MODEL, ["neg_root_mean_squared_error"]),
scorers=safe_scoring[TREATMENT_MODEL],
model_kind=TREATMENT_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=True,
is_treatment_model=True,
)

return variant_outcome_evaluation | propensity_evaluation | treatment_evaluation
Expand Down
29 changes: 26 additions & 3 deletions metalearners/metalearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
OosMethod,
Params,
PredictMethod,
Scoring,
SplitIndices,
Vector,
_ScikitModel,
)
from metalearners._utils import (
default_metric,
index_matrix,
validate_model_and_predict_method,
validate_number_positive,
Expand Down Expand Up @@ -142,7 +144,7 @@ def _evaluate_model_kind(
scorers: Sequence[str | Callable],
model_kind: str,
is_oos: bool,
is_treatment: bool,
is_treatment_model: bool,
oos_method: OosMethod = OVERALL,
) -> dict[str, float]:
"""Helper function to evaluate all the models of the same model kind."""
Expand All @@ -156,7 +158,7 @@ def _evaluate_model_kind(
scorer_name = f"custom_scorer_{idx}"
scorer_callable = scorer
for i, cfe in enumerate(cfes):
if is_treatment:
if is_treatment_model:
treatment_variant = i + 1
index_str = f"{treatment_variant}_vs_0_"
else:
Expand Down Expand Up @@ -877,7 +879,7 @@ def evaluate(
for the possible values.
* ``Callable`` with signature ``scorer(estimator, X, y_true, **kwargs)``. We recommend
using `sklearn.metrics.make_scorer <https://scikit-learn.org/stable/modules/generated/sklearn.metrics.make_scorer.html>`_
to create this callables.
to create such a ``Callable``.
If some model name is not present in the keys of ``scoring`` then the default used
metrics will be ``neg_log_loss`` if it is a classifier and ``neg_root_mean_squared_error``
Expand Down Expand Up @@ -1025,6 +1027,27 @@ def shap_values(
shap_explainer_params=shap_explainer_params,
)

def _scoring(self, scoring: Scoring | None) -> Scoring:

def _default_scoring() -> Scoring:
return {
nuisance_model: [
default_metric(model_specifications["predict_method"](self))
]
for nuisance_model, model_specifications in self.nuisance_model_specifications().items()
} | {
treatment_model: [
default_metric(model_specifications["predict_method"](self))
]
for treatment_model, model_specifications in self.treatment_model_specifications().items()
}

default_scoring = _default_scoring()

if scoring is None:
return default_scoring
return dict(default_scoring) | dict(scoring)


class _ConditionalAverageOutcomeMetaLearner(MetaLearner, ABC):

Expand Down
19 changes: 7 additions & 12 deletions metalearners/rlearner.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# # Copyright (c) QuantCo 2024-2024
# # SPDX-License-Identifier: BSD-3-Clause

from collections.abc import Callable, Mapping

import numpy as np
from joblib import Parallel, delayed
from sklearn.metrics import root_mean_squared_error
from typing_extensions import Self

from metalearners._typing import Matrix, OosMethod, Vector
from metalearners._typing import Matrix, OosMethod, Scoring, Vector
from metalearners._utils import (
clip_element_absolute_value_to_epsilon,
copydoc,
Expand Down Expand Up @@ -335,37 +334,33 @@ def evaluate(
w: Vector,
is_oos: bool,
oos_method: OosMethod = OVERALL,
scoring: Mapping[str, list[str | Callable]] | None = None,
scoring: Scoring | None = None,
) -> dict[str, float]:
"""In the RLearner case, the ``"treatment_model"`` is always evaluated with the
:func:`~metalearners.rlearner.r_loss` and the ``scoring["treatment_model"]``
parameter is ignored."""
if scoring is None:
scoring = {}
safe_scoring = self._scoring(scoring)

propensity_evaluation = _evaluate_model_kind(
cfes=self._nuisance_models[PROPENSITY_MODEL],
Xs=[X],
ys=[w],
scorers=scoring.get(PROPENSITY_MODEL, ["neg_log_loss"]),
scorers=safe_scoring[PROPENSITY_MODEL],
model_kind=PROPENSITY_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=False,
is_treatment_model=False,
)

default_metric = (
"neg_log_loss" if self.is_classification else "neg_root_mean_squared_error"
)
outcome_evaluation = _evaluate_model_kind(
cfes=self._nuisance_models[OUTCOME_MODEL],
Xs=[X],
ys=[y],
scorers=scoring.get(OUTCOME_MODEL, [default_metric]),
scorers=safe_scoring[OUTCOME_MODEL],
model_kind=OUTCOME_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=False,
is_treatment_model=False,
)

# TODO: improve this? generalize it to other metalearners?
Expand Down
16 changes: 5 additions & 11 deletions metalearners/slearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
# # SPDX-License-Identifier: BSD-3-Clause

import warnings
from collections.abc import Callable, Mapping

import numpy as np
import pandas as pd
from typing_extensions import Self

from metalearners._typing import Matrix, OosMethod, Vector
from metalearners._typing import Matrix, OosMethod, Scoring, Vector
from metalearners._utils import (
convert_treatment,
get_one,
Expand Down Expand Up @@ -158,14 +157,9 @@ def evaluate(
w: Vector,
is_oos: bool,
oos_method: OosMethod = OVERALL,
scoring: Mapping[str, list[str | Callable]] | None = None,
scoring: Scoring | None = None,
) -> dict[str, float]:
if scoring is None:
scoring = {}

default_metric = (
"neg_log_loss" if self.is_classification else "neg_root_mean_squared_error"
)
safe_scoring = self._scoring(scoring)

X_with_w = _append_treatment_to_covariates(
X, w, self._supports_categoricals, self.n_variants
Expand All @@ -174,11 +168,11 @@ def evaluate(
cfes=self._nuisance_models[_BASE_MODEL],
Xs=[X_with_w],
ys=[y],
scorers=scoring.get(_BASE_MODEL, [default_metric]),
scorers=safe_scoring[_BASE_MODEL],
model_kind=_BASE_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=False,
is_treatment_model=False,
)

def predict_conditional_average_outcomes(
Expand Down
20 changes: 5 additions & 15 deletions metalearners/tlearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
# # SPDX-License-Identifier: BSD-3-Clause


from collections.abc import Callable, Mapping

import numpy as np
from joblib import Parallel, delayed
from typing_extensions import Self

from metalearners._typing import Matrix, OosMethod, Vector
from metalearners._typing import Matrix, OosMethod, Scoring, Vector
from metalearners._utils import index_matrix
from metalearners.cross_fit_estimator import OVERALL
from metalearners.metalearner import (
Expand Down Expand Up @@ -116,25 +114,17 @@ def evaluate(
w: Vector,
is_oos: bool,
oos_method: OosMethod = OVERALL,
scoring: Mapping[str, list[str | Callable]] | None = None,
scoring: Scoring | None = None,
) -> dict[str, float]:
if scoring is None:
scoring = {}

default_metric = (
"neg_log_loss" if self.is_classification else "neg_root_mean_squared_error"
)
safe_scoring = self._scoring(scoring)

masks = []
for tv in range(self.n_variants):
masks.append(w == tv)
return _evaluate_model_kind(
cfes=self._nuisance_models[VARIANT_OUTCOME_MODEL],
Xs=[X[w == tv] for tv in range(self.n_variants)],
ys=[y[w == tv] for tv in range(self.n_variants)],
scorers=scoring.get(VARIANT_OUTCOME_MODEL, [default_metric]),
scorers=safe_scoring[VARIANT_OUTCOME_MODEL],
model_kind=VARIANT_OUTCOME_MODEL,
is_oos=is_oos,
oos_method=oos_method,
is_treatment=False,
is_treatment_model=False,
)
Loading

0 comments on commit 6771e5a

Please sign in to comment.