diff --git a/README.md b/README.md index 77985b41..8dfd8f7a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Its estimators are taken from [EconML](https://github.com/microsoft/EconML/) aug [DoWhy](https://github.com/microsoft/DoWhy/) wrapper. Our contribution is enabling automatic estimator tuning and selection by out-of-sample scoring of causal estimators, notably using the [energy score](https://arxiv.org/abs/2212.10076). -We use [FLAML](https://github.com/microsoft/FLAML) for hyperparameter optimisation. +For hyperparameter optimisation we support pluggable backends — [Optuna](https://optuna.org) (default), [Hyperopt](https://github.com/hyperopt/hyperopt), and [FLAML](https://github.com/microsoft/FLAML) — selectable via the `framework` argument to `fit()`. We perform automated hyperparameter tuning of first stage models (for the treatment and outcome models) as well as hyperparameter tuning and model selection for the second stage model (causal estimator). @@ -122,7 +122,7 @@ To install from source, see [For Developers](#for-developers) section below. ### Requirements -CausalTune works with Python 3.8 and 3.9. +CausalTune works with Python 3.10, 3.11 and 3.12. It requires the following libraries to work: - NumPy @@ -130,9 +130,13 @@ It requires the following libraries to work: - EconML - DoWhy - FLAML +- Optuna - Scikit-Learn - Dcor +Hyperopt is an optional backend, installed via the `hyperopt` extra +(`pip install causaltune[hyperopt]`). + The easiest way to install the dependencies is via ``` pip install -r requirements.txt @@ -147,7 +151,7 @@ Mac/ OS users: For some machines, it can happen that the package LightGBM which 2. Set Up a Conda Environment using an appropriate Python Version - Ensure Anaconda or Miniconda is installed. - - Create a new Conda environment: `conda create -n causaltune-env python=3.9.x` + - Create a new Conda environment: `conda create -n causaltune-env python=3.11.x` - Activate the environment: `conda activate causaltune-env`. 3. Install the dependency lightgbm seperatly before attempting to install other dependencies @@ -190,6 +194,13 @@ print(f"Best estimator: {ct.best_estimator}") ``` +By default `fit()` optimises with the Optuna backend. Pass `framework="hyperopt"` +or `framework="flaml"` to switch, and `algo=` to pick a specific sampler / +search algorithm for that backend (e.g. an Optuna sampler, a Hyperopt suggest +function, or a FLAML search algorithm). Warm starting via `try_init_configs` and +resuming a previous fit with `resume=True` are currently supported only with +`framework="flaml"`. + Now if ***outcome_model="auto"*** in the CausalTune constructor, we search over a simultaneous search space for the EconML estimators and for FLAML wrappers for common regressors. The old behavior is now achieved by ***outcome_model="nested"*** (Refitting AutoML for each estimator). You can also preprocess the data in the CausalityDataset using one of the popular category encoders: ***OneHot, WoE, Label, Target***. diff --git a/causaltune/optimiser.py b/causaltune/optimiser.py index 4df5a434..adacb6b6 100644 --- a/causaltune/optimiser.py +++ b/causaltune/optimiser.py @@ -1,6 +1,6 @@ import copy import warnings -from typing import List, Optional, Union +from typing import Any, List, Optional, Union from collections import defaultdict import time @@ -8,7 +8,7 @@ import pandas as pd import numpy as np from sklearn.linear_model import _base -from flaml import tune +from hiertunehub import create_tuner from sklearn.dummy import DummyClassifier from sklearn.model_selection import train_test_split @@ -176,6 +176,7 @@ def __init__( self._settings["tuner"]["resources_per_trial"] = ( resources_per_trial if resources_per_trial is not None else {"cpu": 0.5} ) + self._settings["tuner"]["algo"] = None self._settings["try_init_configs"] = try_init_configs self._settings[ "include_experimental_estimators" @@ -207,7 +208,7 @@ def __init__( self._settings["propensity_model"] = propensity_model self._settings["outcome_model"] = outcome_model - self.results = None + self.tuner = None self._best_estimators = defaultdict(lambda: (float("-inf"), None)) self.original_estimator_list = estimator_list @@ -216,9 +217,6 @@ def __init__( self.identified_estimand = None self.problem = None self.use_ray = use_ray - # properties that are used to resume fits (warm start) - self.resume_scores = [] - self.resume_cfg = [] def get_params(self, deep=False): return self._settings.copy() @@ -306,6 +304,8 @@ def fit( encoder_type: Optional[str] = None, encoder_outcome: Optional[str] = None, use_ray: Optional[bool] = None, + framework: Optional[str] = "optuna", + algo: Any = None, ): """Performs AutoML on list of causal inference estimators - If estimator has a search space specified in its parameters, HPO is performed on the whole model. @@ -325,6 +325,13 @@ def fit( preprocess (bool): preprocess CausalityDataset if needed. encoder_type (Optional[str]): Categorical Encoder for preprocessing encoder_outcome (Optional[str]): Categorical Encoder target for preprocessing: TargetEncoder, WOE. + framework (Optional[str]): HPO backend to use, one of "optuna" + (default), "hyperopt" or "flaml". Only "flaml" supports + try_init_configs warm-start and resume; the others warn/raise. + algo (Any): search algorithm for the chosen backend. flaml -> a + FLAML search_alg; hyperopt -> a suggest function (defaults to + hyperopt.tpe.suggest); optuna -> an optuna sampler (defaults to + optuna's TPESampler). None uses each backend's default. Returns: None @@ -494,70 +501,123 @@ def fit( search_space = self.cfg.search_space( self.estimator_list, data_size=data.data.shape ) + # init configs (warm-start points) are only wired for the FLAML backend; + # for hyperopt/optuna they are a best-effort no-op (warn once). + if self._settings["try_init_configs"] and framework != "flaml": + warnings.warn( + "try_init_configs (init config warm-start) is only applied with " + f"framework='flaml'; ignored for framework='{framework}'.", + UserWarning, + ) init_cfg = ( self.cfg.default_configs(self.estimator_list, data_size=data.data.shape) - if self._settings["try_init_configs"] + if self._settings["try_init_configs"] and framework == "flaml" else [] ) - if resume and self.results: - # pull out configs and resume_scores from previous trials: - for _, result in self.results.results.items(): - self.resume_scores.append(result[self.metric]) - self.resume_cfg.append(result["config"]) - # append init_cfgs that have not yet been evaluated - for cfg in init_cfg: - self.resume_cfg.append(cfg) if cfg not in self.resume_cfg else None - try: - self.results = tune.run( - self._tune_with_config, - search_space, - metric=self.metric, - # use_ray=self.use_ray, - cost_attr="evaluation_cost", - points_to_evaluate=( - init_cfg if len(self.resume_cfg) == 0 else self.resume_cfg - ), - evaluated_rewards=( - [] if len(self.resume_scores) == 0 else self.resume_scores - ), - mode=("min" if self.metric in metrics_to_minimize() else "max"), - # resources_per_trial= {"cpu": 1} if self.use_ray else None, - low_cost_partial_config={}, - **self._settings["tuner"], + if resume and framework != "flaml": + raise NotImplementedError( + "resume is currently only supported with framework='flaml', " + f"not framework='{framework}'." ) - if self.results.get_best_trial() is None: - raise Exception( - "Optimization failed! Did you set large enough time_budget and components_budget?" + if framework == "hyperopt" and not self._search_space_has_tunable_params(): + raise ValueError( + "framework='hyperopt' needs at least one tunable hyperparameter " + "in the search space, but all selected estimators are " + "parameterless and outcome_model is not 'auto'. (hiertunehub " + "0.2.1's hyperopt conversion raises on a fully parameterless " + "search.) Use framework='optuna' or 'flaml', include a " + "parameterized estimator, or set outcome_model='auto'." + ) + + self._settings["tuner"]["algo"] = algo + mode = "min" if self.metric in metrics_to_minimize() else "max" + framework_params = self.cfg.parse_tuner_params( + self._settings["tuner"], framework + ) + + if framework == "flaml": + # Restore full FLAML parity: cost-aware search plus warm-start / + # resume seeds. Capture resume points/rewards from the PREVIOUS tuner + # before it is overwritten below. + if resume and self.tuner is not None: + points_to_evaluate, evaluated_rewards = self._resume_points_and_rewards( + self.tuner.results, init_cfg ) - except Exception: - # we must have an older FLAML version that doesn't support the cost_attr parameter - self.results = tune.run( - self._tune_with_config, - search_space, - metric=self.metric, - points_to_evaluate=( - init_cfg if len(self.resume_cfg) == 0 else self.resume_cfg - ), - evaluated_rewards=( - [] if len(self.resume_scores) == 0 else self.resume_scores - ), - mode=("min" if self.metric in metrics_to_minimize() else "max"), + else: + points_to_evaluate, evaluated_rewards = init_cfg, [] + framework_params.update( + cost_attr="evaluation_cost", low_cost_partial_config={}, - **self._settings["tuner"], + points_to_evaluate=points_to_evaluate, + evaluated_rewards=evaluated_rewards, ) - # print("Optimization failed!\n", traceback.format_exc()) - # raise e + + self.tuner = create_tuner( + self._tune_with_config, + search_space, + metric=self.metric, + mode=mode, + framework=framework, + framework_params=framework_params, + ) + self.tuner.run() + self.update_summary_scores() + def _resume_points_and_rewards(self, prev_results, init_cfg): + """Rebuild FLAML warm-start seeds from a previous tuner's results. + + Mirrors the original resume semantics: for each prior trial that carries + both the metric and its config, seed ``(config -> reward)``; then append + any init configs not already present (without a reward, so FLAML + evaluates them). + + Args: + prev_results (list[dict]): ``tuner.results`` from the previous fit. + init_cfg (list[dict]): init configs to append if not yet evaluated. + + Returns: + tuple[list[dict], list]: ``(points_to_evaluate, evaluated_rewards)`` + with rewards aligned to the leading resumed configs. + """ + resume_cfg = [] + resume_scores = [] + for result in prev_results: + if self.metric not in result or "config" not in result: + continue + resume_scores.append(result[self.metric]) + resume_cfg.append(result["config"]) + for cfg in init_cfg: + if cfg not in resume_cfg: + resume_cfg.append(cfg) + return resume_cfg, resume_scores + + def _search_space_has_tunable_params(self) -> bool: + """Whether the current search space contains any tunable hyperparameter. + + Sampling outcome estimators (outcome_model='auto') always adds tunable + component-model params; otherwise it depends on the estimators' own + search spaces. Used to guard the hyperopt backend, whose hiertunehub + converter raises on a fully parameterless search space. + """ + if self.cfg.sample_outcome_estimators: + return True + configs = self.cfg._configs() + return any( + bool(configs[est].search_space) + for est in self.estimator_list + if est in configs + ) + def update_summary_scores(self): """Stores scores for metric of interest for each estimator Returns: None """ - self.scores = Scorer.best_score_by_estimator(self.results.results, self.metric) + self.scores = Scorer.best_score_by_estimator(self.tuner.results, self.metric) # now inject the separately saved model objects for est_name in self.scores: # Todo: Check approximate scores for OrthoIV (possibly other IV estimators) @@ -710,8 +770,14 @@ def _estimate_effect(self, config): } except Exception as e: print("Evaluation failed!\n", config, traceback.format_exc()) + # Use the *worst* value for the metric direction as the failure + # sentinel, so a failed trial is never picked as best. For minimized + # metrics (e.g. energy_distance) that is +inf; for maximized metrics + # it is -inf. (A flat -inf would look optimal to a minimizing backend + # such as the default optuna, poisoning best_estimator selection.) + worst = np.inf if self.metric in metrics_to_minimize() else -np.inf return { - self.metric: -np.inf, + self.metric: worst, "estimator_name": self.estimator_name, "exception": e, "traceback": traceback.format_exc(), @@ -750,7 +816,7 @@ def best_estimator(self) -> str: Returns: None """ - return self.results.best_result["estimator_name"] + return self.tuner.best_result["estimator_name"] @property def model(self): @@ -759,7 +825,10 @@ def model(self): Returns: CausalEstimator """ - return self.results.best_result["estimator"].estimator + # The objective pops the fitted estimator out of the result dict in the + # non-store_all path, so resolve it from the scores table (populated by + # update_summary_scores) rather than from tuner.best_result. + return self.scores[self.best_estimator]["estimator"].estimator def best_model_for_estimator(self, estimator_name): """Return the best model found for a particular estimator. @@ -781,7 +850,7 @@ def best_config(self): Returns: (dict): the best configuration """ - return self.results.best_config + return self.tuner.best_params @property def best_config_per_estimator(self): @@ -800,7 +869,7 @@ def best_score(self): """ Returns: (float): the best score found.""" - return self.results.best_result[self.metric] + return self.tuner.best_result[self.metric] def effect(self, df, *args, **kwargs): """Heterogeneous Treatment Effects for data df diff --git a/causaltune/score/scoring.py b/causaltune/score/scoring.py index d4452d8a..5e6ed490 100644 --- a/causaltune/score/scoring.py +++ b/causaltune/score/scoring.py @@ -1307,13 +1307,12 @@ def make_scores( return out @staticmethod - def best_score_by_estimator( - scores: Dict[str, dict], metric: str - ) -> Dict[str, dict]: + def best_score_by_estimator(scores: List[dict], metric: str) -> Dict[str, dict]: """Obtain best score for each estimator. Args: - scores (Dict[str, dict]): CausalTune.scores dictionary + scores (List[dict]): list of per-trial result dicts (as produced by + the tuner), each carrying an ``estimator_name`` field metric (str): metric of interest Returns: @@ -1321,29 +1320,20 @@ def best_score_by_estimator( """ - for k, v in scores.items(): + for v in scores: if "estimator_name" not in v: raise ValueError( - f"Malformed scores dict, 'estimator_name' field missing " - f"in{k}, {v}" + f"Malformed scores entry, 'estimator_name' field missing " f"in {v}" ) estimator_names = sorted( - list( - set( - [ - v["estimator_name"] - for v in scores.values() - if "estimator_name" in v - ] - ) - ) + list(set([v["estimator_name"] for v in scores if "estimator_name" in v])) ) best = {} for name in estimator_names: est_scores = [ v - for v in scores.values() + for v in scores if "estimator_name" in v and v["estimator_name"] == name ] best[name] = ( diff --git a/causaltune/search/params.py b/causaltune/search/params.py index 6f0e335b..2f21633b 100644 --- a/causaltune/search/params.py +++ b/causaltune/search/params.py @@ -7,6 +7,7 @@ import warnings from econml.inference import BootstrapInference # noqa F401 from sklearn import linear_model +from hiertunehub import SearchSpace from causaltune.utils import clean_config from causaltune.search.component import model_from_cfg, joint_config @@ -161,7 +162,73 @@ def search_space( data_size, outcome_estimator_list ) - return out + return SearchSpace.from_flaml(out, name="estimator_name") + + @staticmethod + def parse_tuner_params(params: dict, framework: str) -> dict: + """Translate CausalTune's tuner settings into the per-framework kwargs + expected by hiertunehub's ``create_tuner``. + + Args: + params (dict): the ``_settings["tuner"]`` dict, carrying + ``num_samples``, ``time_budget_s``, ``verbose``, + ``resources_per_trial`` and ``algo``. + framework (str): one of "flaml", "hyperopt", "optuna". + + Returns: + dict: framework-specific parameters for ``create_tuner``. + + Raises: + ValueError: for an unsupported framework, or for optuna/hyperopt when + the search is unbounded (no ``num_samples`` and no time budget). + ImportError: for hyperopt when the optional extra is not installed. + """ + num_samples = params["num_samples"] + time_budget_s = params["time_budget_s"] + algo = params.get("algo") + + if framework == "flaml": + return { + "num_samples": num_samples, + "time_budget_s": time_budget_s, + "verbose": params["verbose"], + "resources_per_trial": params["resources_per_trial"], + "search_alg": algo, + } + elif framework == "hyperopt": + try: + import hyperopt + except ImportError as e: + raise ImportError( + "hyperopt is not installed. Install it with " + "`pip install causaltune[hyperopt]`." + ) from e + max_evals = num_samples if num_samples != -1 else None + if max_evals is None and time_budget_s is None: + raise ValueError( + "hyperopt requires a bounded search: set either " + "num_samples (!= -1) or a time budget." + ) + return { + "max_evals": max_evals, + "timeout": time_budget_s, + "verbose": params["verbose"], + "algo": algo if algo is not None else hyperopt.tpe.suggest, + } + elif framework == "optuna": + n_trials = num_samples if num_samples != -1 else None + if n_trials is None and time_budget_s is None: + raise ValueError( + "optuna requires a bounded search: set either " + "num_samples (!= -1) or a time budget." + ) + return { + "n_trials": n_trials, + "timeout": time_budget_s, + "sampler": algo, + } + else: + raise ValueError(f"Framework {framework} not supported") def default_configs( self, diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 2af774b8..04c07364 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -45,6 +45,14 @@ The CausalTune package can be used like a scikit-style estimator: print(f"Best estimator: {ct.best_estimator}") +By default ``fit()`` optimises with the Optuna backend. Pass +``framework="hyperopt"`` or ``framework="flaml"`` to switch, and ``algo=`` to +select a specific sampler / search algorithm. Warm starting via +``try_init_configs`` and ``resume=True`` are currently supported only with +``framework="flaml"``. Hyperopt requires the optional extra +(``pip install causaltune[hyperopt]``). + + For Developers ---------------- @@ -70,6 +78,7 @@ CausalTune requires the following packages: * econml * dowhy * flaml +* optuna * scikit-learn * matplotlib * dcor diff --git a/docs/index.rst b/docs/index.rst index d9894c04..dc0a44ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,7 +28,7 @@ Its estimators are taken from EconML_ augmented by a couple of extra models DoWhy_ wrapper. Our contribution is enabling automatic estimator tuning and selection by out-of-sample scoring of causal estimators, notably using the energy_score_. -We use FLAML_ for hyperparameter optimisation. +For hyperparameter optimisation we support pluggable backends — Optuna_ (default), Hyperopt_, and FLAML_ — selectable via the ``framework`` argument to ``fit()``. We perform automated hyperparameter tuning of first stage models (for the treatment and outcome models) as well as hyperparameter tuning and model selection for the second stage model (causal estimator). @@ -53,6 +53,8 @@ though energy score performed better in our synthetic data experiments. .. _EconML: https://github.com/microsoft/EconML/ .. _FLAML: https://github.com/microsoft/FLAML +.. _Optuna: https://optuna.org +.. _Hyperopt: https://github.com/hyperopt/hyperopt .. _DoWhy: https://github.com/microsoft/DoWhy/ .. _calculation: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3111957 .. _ERUPT: https://medium.com/building-ibotta/erupt-expected-response-under-proposed-treatments-ff7dd45c84b4 diff --git a/notebooks/Run metric tests across different scenarios.ipynb b/notebooks/Run metric tests across different scenarios.ipynb index 8090a721..c55fbead 100644 --- a/notebooks/Run metric tests across different scenarios.ipynb +++ b/notebooks/Run metric tests across different scenarios.ipynb @@ -377,11 +377,11 @@ " # sort trials by validation set performance\n", " # assign trials to estimators\n", " estimator_scores = {est: [] for est in ct.scores.keys() if \"NewDummy\" not in est}\n", - " for trial in ct.results.trials:\n", + " for trial in ct.tuner.trials:\n", " # estimator name:\n", - " estimator_name = trial.last_result[\"estimator_name\"]\n", - " if trial.last_result[\"estimator\"]:\n", - " estimator = trial.last_result[\"estimator\"]\n", + " estimator_name = trial.result[\"estimator_name\"]\n", + " if trial.result[\"estimator\"]:\n", + " estimator = trial.result[\"estimator\"]\n", " scores = {}\n", " for ds_name, df in datasets.items():\n", " scores[ds_name] = {}\n", @@ -397,7 +397,7 @@ " # add ground truth for convenience\n", " scores[ds_name][\"CATE_groundtruth\"] = df[\"true_effect\"]\n", " scores[ds_name][metric] = est_scores[metric]\n", - " scores['optimization_score'] = trial.last_result.get('optimization_score')\n", + " scores['optimization_score'] = trial.result.get('optimization_score')\n", "\n", " estimator_scores[estimator_name].append(scores)\n", "\n", diff --git a/notebooks/RunExperiments/runners/experiment_runner.py b/notebooks/RunExperiments/runners/experiment_runner.py index 3ed090ed..131c4dfd 100644 --- a/notebooks/RunExperiments/runners/experiment_runner.py +++ b/notebooks/RunExperiments/runners/experiment_runner.py @@ -378,11 +378,11 @@ def compute_scores(ct, metric, test_df): estimator_scores = {est: [] for est in ct.scores.keys() if "NewDummy" not in est} all_scores = [] - for trial in ct.results.trials: + for trial in ct.tuner.trials: try: - estimator_name = trial.last_result["estimator_name"] - if "estimator" in trial.last_result and trial.last_result["estimator"]: - estimator = trial.last_result["estimator"] + estimator_name = trial.result["estimator_name"] + if "estimator" in trial.result and trial.result["estimator"]: + estimator = trial.result["estimator"] scores = {} for ds_name, df in datasets.items(): scores[ds_name] = {} @@ -405,7 +405,7 @@ def compute_scores(ct, metric, test_df): ** 2 ) scores[ds_name]["scores"] = est_scores - scores["optimization_score"] = trial.last_result.get("optimization_score") + scores["optimization_score"] = trial.result.get("optimization_score") estimator_scores[estimator_name].append(copy.deepcopy(scores)) # Will use this in the nex all_scores.append(scores) diff --git a/notebooks/paper_submission/confounded_run.py b/notebooks/paper_submission/confounded_run.py index 38b56363..3c89cc15 100644 --- a/notebooks/paper_submission/confounded_run.py +++ b/notebooks/paper_submission/confounded_run.py @@ -89,11 +89,11 @@ # # assign trials to estimators # # estimator_scores = {est: [] for est in ct.scores.keys() if "NewDummy" not in est} # -# for trial in ct.results.trials: +# for trial in ct.tuner.trials: # # estimator name: -# estimator_name = trial.last_result["estimator_name"] -# if trial.last_result.get("estimator", False): -# estimator = trial.last_result["estimator"] +# estimator_name = trial.result["estimator_name"] +# if trial.result.get("estimator", False): +# estimator = trial.result["estimator"] # scores = {} # for ds_name, df in datasets.items(): # scores[ds_name] = {} diff --git a/notebooks/paper_submission/notebooks/example_qini.ipynb b/notebooks/paper_submission/notebooks/example_qini.ipynb index b2ab1ed8..90ee7d0b 100644 --- a/notebooks/paper_submission/notebooks/example_qini.ipynb +++ b/notebooks/paper_submission/notebooks/example_qini.ipynb @@ -332,11 +332,11 @@ "# sort trials by validation set performance\n", "# assign trials to estimators\n", "estimator_scores = {est: [] for est in ct.scores.keys() if \"NewDummy\" not in est}\n", - "for trial in ct.results.trials:\n", + "for trial in ct.tuner.trials:\n", " # estimator name:\n", - " estimator_name = trial.last_result[\"estimator_name\"]\n", - " if trial.last_result[\"estimator\"]:\n", - " estimator = trial.last_result[\"estimator\"]\n", + " estimator_name = trial.result[\"estimator_name\"]\n", + " if trial.result[\"estimator\"]:\n", + " estimator = trial.result[\"estimator\"]\n", " scores = {}\n", " for ds_name, df in datasets.items():\n", " scores[ds_name] = {}\n", diff --git a/notebooks/paper_submission/notebooks/example_synthetic_cate_observational.ipynb b/notebooks/paper_submission/notebooks/example_synthetic_cate_observational.ipynb index a33c7160..09900750 100644 --- a/notebooks/paper_submission/notebooks/example_synthetic_cate_observational.ipynb +++ b/notebooks/paper_submission/notebooks/example_synthetic_cate_observational.ipynb @@ -538,11 +538,11 @@ " # sort trials by validation set performance\n", " # assign trials to estimators\n", " estimator_scores = {est: [] for est in ct.scores.keys() if \"NewDummy\" not in est}\n", - " for trial in ct.results.trials:\n", + " for trial in ct.tuner.trials:\n", " # estimator name:\n", - " estimator_name = trial.last_result[\"estimator_name\"]\n", - " if trial.last_result[\"estimator\"]:\n", - " estimator = trial.last_result[\"estimator\"]\n", + " estimator_name = trial.result[\"estimator_name\"]\n", + " if trial.result[\"estimator\"]:\n", + " estimator = trial.result[\"estimator\"]\n", " scores = {}\n", " for ds_name, df in datasets.items():\n", " scores[ds_name] = {}\n", diff --git a/notebooks/paper_submission/notebooks/example_synthetic_cate_rct.ipynb b/notebooks/paper_submission/notebooks/example_synthetic_cate_rct.ipynb index 16e4068e..e1d1f5f9 100644 --- a/notebooks/paper_submission/notebooks/example_synthetic_cate_rct.ipynb +++ b/notebooks/paper_submission/notebooks/example_synthetic_cate_rct.ipynb @@ -498,11 +498,11 @@ " # sort trials by validation set performance\n", " # assign trials to estimators\n", " estimator_scores = {est: [] for est in ct.scores.keys() if \"NewDummy\" not in est}\n", - " for trial in ct.results.trials:\n", + " for trial in ct.tuner.trials:\n", " # estimator name:\n", - " estimator_name = trial.last_result[\"estimator_name\"]\n", - " if trial.last_result[\"estimator\"]:\n", - " estimator = trial.last_result[\"estimator\"]\n", + " estimator_name = trial.result[\"estimator_name\"]\n", + " if trial.result[\"estimator\"]:\n", + " estimator = trial.result[\"estimator\"]\n", " scores = {}\n", " for ds_name, df in datasets.items():\n", " scores[ds_name] = {}\n", diff --git a/notebooks/paper_submission/notebooks/example_synthetic_instrumental_variable.ipynb b/notebooks/paper_submission/notebooks/example_synthetic_instrumental_variable.ipynb index ac6d35cf..04924d02 100644 --- a/notebooks/paper_submission/notebooks/example_synthetic_instrumental_variable.ipynb +++ b/notebooks/paper_submission/notebooks/example_synthetic_instrumental_variable.ipynb @@ -551,7 +551,7 @@ } ], "source": [ - "dir(ct.results.trials[0])" + "dir(ct.tuner.trials[0])" ] }, { @@ -571,7 +571,7 @@ } ], "source": [ - "ct.results.trials[0].last_result.keys()" + "ct.tuner.trials[0].result.keys()" ] }, { diff --git a/poetry.lock b/poetry.lock index 2294a3a2..43268f49 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,26 @@ # This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +[[package]] +name = "alembic" +version = "1.18.5" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc"}, + {file = "alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.4.23" +tomli = {version = "*", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.12" + +[package.extras] +tz = ["tzdata"] + [[package]] name = "altair" version = "6.2.2" @@ -591,6 +612,24 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "colorlog" +version = "6.10.1" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"}, + {file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + [[package]] name = "comm" version = "0.2.3" @@ -1475,6 +1514,19 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd ; python_version < \"3.14\"", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas (<3.0.0)", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr (<3.2.0)", "zstandard ; python_version < \"3.14\""] tqdm = ["tqdm"] +[[package]] +name = "future" +version = "1.0.0" +description = "Clean single-source support for Python 3 and 2" +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +markers = "extra == \"hyperopt\"" +files = [ + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -1526,6 +1578,100 @@ dev = ["Flake8-pyproject", "build", "flake8", "pep8-naming", "tox (>=3)", "twine docs = ["sphinx (>=5,<7)", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=0.2.5)"] test = ["coverage", "pytest (>=7,<8.1)", "pytest-cov", "pytest-mock (>=3)"] +[[package]] +name = "greenlet" +version = "3.5.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" +files = [ + {file = "greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71"}, + {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8"}, + {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702"}, + {file = "greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db"}, + {file = "greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c"}, + {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8"}, + {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8"}, + {file = "greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7"}, + {file = "greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44"}, + {file = "greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c"}, + {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149"}, + {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea"}, + {file = "greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c"}, + {file = "greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d"}, + {file = "greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda"}, + {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb"}, + {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b"}, + {file = "greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b"}, + {file = "greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4"}, + {file = "greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260"}, + {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a"}, + {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154"}, + {file = "greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e"}, + {file = "greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605"}, + {file = "greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0"}, + {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21"}, + {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da"}, + {file = "greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3"}, + {file = "greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128"}, + {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34"}, + {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b"}, + {file = "greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930"}, + {file = "greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227"}, + {file = "greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d"}, + {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb"}, + {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16"}, + {file = "greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf"}, + {file = "greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31"}, + {file = "greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + [[package]] name = "h11" version = "0.16.0" @@ -1538,6 +1684,25 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "hiertunehub" +version = "0.2.1" +description = "A Python package to provide unified interface for hierarchical hyperparameter optimization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "hiertunehub-0.2.1-py3-none-any.whl", hash = "sha256:d35966d62d8e016f2e79a971487ea4812c626d19b7b4325ad3a56c6c4e57c910"}, + {file = "hiertunehub-0.2.1.tar.gz", hash = "sha256:a4a961709b4682ca66107eaab9c8d1517855cb4459d318feae091ed7dba6cae7"}, +] + +[package.dependencies] +PyYAML = "*" + +[package.extras] +opt = ["flaml", "hyperopt", "optuna"] +test = ["pytest"] + [[package]] name = "highspy" version = "1.15.1" @@ -1692,6 +1857,35 @@ files = [ {file = "httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999"}, ] +[[package]] +name = "hyperopt" +version = "0.2.7" +description = "Distributed Asynchronous Hyperparameter Optimization" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"hyperopt\"" +files = [ + {file = "hyperopt-0.2.7-py2.py3-none-any.whl", hash = "sha256:f3046d91fe4167dbf104365016596856b2524a609d22f047a066fc1ac796427c"}, + {file = "hyperopt-0.2.7.tar.gz", hash = "sha256:1bf89ae58050bbd32c7307199046117feee245c2fd9ab6255c7308522b7ca149"}, +] + +[package.dependencies] +cloudpickle = "*" +future = "*" +networkx = ">=2.2" +numpy = "*" +py4j = "*" +scipy = "*" +six = "*" +tqdm = "*" + +[package.extras] +atpe = ["lightgbm", "scikit-learn"] +dev = ["black", "nose", "pre-commit", "pytest"] +mongotrials = ["pymongo"] +sparktrials = ["pyspark"] + [[package]] name = "idna" version = "3.18" @@ -2224,6 +2418,26 @@ files = [ {file = "logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047"}, ] +[[package]] +name = "mako" +version = "1.3.12" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"}, + {file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -2894,6 +3108,31 @@ files = [ {file = "nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:8ce1b8213f61f2bfac132e6df890af6450b77cbd140c6ce4e98cb0c2d8e678c9"}, ] +[[package]] +name = "optuna" +version = "4.9.0" +description = "A hyperparameter optimization framework" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "optuna-4.9.0-py3-none-any.whl", hash = "sha256:f52f3be6148654850c92a5860d398fd88ec6b2c84ab68d9c3d07dcff02e7afee"}, + {file = "optuna-4.9.0.tar.gz", hash = "sha256:b322e5cbdf1655fb84c37646c4a7a1f391de1b47806bbe222e015825d0a82b87"}, +] + +[package.dependencies] +alembic = ">=1.5.0" +colorlog = "*" +numpy = "*" +packaging = ">=20.0" +PyYAML = "*" +sqlalchemy = ">=1.4.2" +tqdm = "*" + +[package.extras] +document = ["ase", "cmaes (>=0.12.0)", "fvcore", "kaleido (!=0.2.1.post1,<0.4)", "lightgbm", "matplotlib (!=3.6.0)", "pandas", "pillow", "plotly (>=4.9.0)", "scikit-learn", "sphinx", "sphinx-copybutton", "sphinx-gallery", "sphinx-notfound-page", "sphinx_rtd_theme (>=1.2.0)", "torch", "torchvision"] +optional = ["boto3", "cmaes (>=0.12.0)", "google-cloud-storage", "greenlet", "grpcio", "matplotlib (!=3.6.0)", "pandas", "plotly (>=4.9.0)", "protobuf (>=5.28.1)", "redis", "scikit-learn (>=0.24.2)", "scipy", "torch"] + [[package]] name = "orjson" version = "3.11.9" @@ -3490,6 +3729,19 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "py4j" +version = "0.10.9.9" +description = "Enables Python programs to dynamically access arbitrary Java objects" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"hyperopt\"" +files = [ + {file = "py4j-0.10.9.9-py2.py3-none-any.whl", hash = "sha256:c7c26e4158defb37b0bb124933163641a2ff6e3a3913f7811b0ddbe07ed61533"}, + {file = "py4j-0.10.9.9.tar.gz", hash = "sha256:f694cad19efa5bd1dee4f3e5270eb406613c974394035e5bfc4ec1aba870b879"}, +] + [[package]] name = "pyarrow" version = "24.0.0" @@ -3906,10 +4158,9 @@ files = [ name = "pyyaml" version = "6.0.3" description = "YAML parser and emitter for Python" -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"ray\"" files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -4707,24 +4958,24 @@ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] [[package]] name = "setuptools" -version = "83.0.0" -description = "Most extensible Python build backend with support for C/C++ extension modules" +version = "80.10.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"}, - {file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"}, + {file = "setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173"}, + {file = "setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=3.4)"] +enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "shap" @@ -5018,6 +5269,103 @@ files = [ [package.dependencies] numpy = ">=2.0.0" +[[package]] +name = "sqlalchemy" +version = "2.0.51" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win32.whl", hash = "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win_amd64.whl", hash = "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win32.whl", hash = "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win_amd64.whl", hash = "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7"}, + {file = "sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5"}, + {file = "sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "stack-data" version = "0.6.3" @@ -5619,10 +5967,11 @@ pyspark = ["cloudpickle", "pyspark", "scikit-learn"] scikit-learn = ["scikit-learn"] [extras] +hyperopt = ["hyperopt", "setuptools"] ray = ["ray"] test = ["autoflake", "black", "flake8", "isort", "nbmake", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "d9749a6ef13dccc8a2ed436e16a36db2e88044b1121f290799990f7c9d6b7110" +content-hash = "27c9075fcbc251200b6b848f43959e33d8f6ce10c9e29648716f778706f7666d" diff --git a/pyproject.toml b/pyproject.toml index 588a587b..be9a42a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,10 +20,14 @@ dependencies = [ "FLAML==2.6.0", "xgboost==2.1.4", "numpy==2.2.6", + # Unified HPO-backend interface (young package): pinned exactly. + "hiertunehub==0.2.1", # Loosely-coupled: floors (with caps where an upstream ceiling is real). "pandas>=2,<3", "scikit-learn>=1.4,<1.7", "category-encoders>=2.6.3", + # Default HPO backend (numpy-2 clean from 4.0): hard dep. + "optuna>=4.0", "pytest", "matplotlib", "dcor", @@ -42,6 +46,9 @@ test = [ "nbmake", ] ray = ["ray[tune]>=2.9"] +# Optional HPO backend. hyperopt 0.2.7 imports the deprecated pkg_resources, +# removed in setuptools 81, so the setuptools ceiling ships with this extra. +hyperopt = ["hyperopt>=0.2.7", "setuptools<81"] [project.urls] Homepage = "https://github.com/py-why/causaltune" diff --git a/tests/causaltune/test_erupt_core.py b/tests/causaltune/test_erupt_core.py index da8aa7c2..80661dc4 100644 --- a/tests/causaltune/test_erupt_core.py +++ b/tests/causaltune/test_erupt_core.py @@ -43,6 +43,11 @@ def evaluate_erupt_new( def make_dataset(n: int = 10000): + # Seed the global RNG: these tests compare an ERUPT estimate against an + # empirical mean with a tight tolerance (atol=5e-2), and unseeded per-process + # randomness made them intermittently fail in CI. A fixed seed keeps them + # deterministic (seed 0 leaves ~16x margin on the tightest assertion). + np.random.seed(0) # Let's create a dataset with a single feature df = pd.DataFrame({"X": np.random.uniform(size=n)}) diff --git a/tests/causaltune/test_tuner_backends.py b/tests/causaltune/test_tuner_backends.py new file mode 100644 index 00000000..67312629 --- /dev/null +++ b/tests/causaltune/test_tuner_backends.py @@ -0,0 +1,482 @@ +"""Tests for the pluggable HPO backends (flaml / hyperopt / optuna) that replace +the old FLAML-only ``tune.run`` surface, ported from PR #337. + +These cover: +* ``SimpleParamService.parse_tuner_params`` mapping for each framework, incl. + the ``num_samples == -1 -> None`` rule, the hyperopt ``algo`` default, exact + key sets (no FLAML-only kwargs leaking into optuna/hyperopt), the + unsupported-framework ``ValueError`` and the unbounded-budget guard. +* ``SimpleParamService.search_space`` now returning a ``hiertunehub.SearchSpace`` + that round-trips back to the original FLAML ``{"estimator": choice([...])}``. +* ``Scorer.best_score_by_estimator`` accepting a *list* of result dicts (and + raising ``ValueError`` -- not ``NameError`` -- on a malformed entry). +* End-to-end ``CausalTune.fit(framework=...)`` for all three backends, asserting + the tuner is a real ``hiertunehub`` Tuner and the public properties are + repointed to it. +* The new default backend being optuna, and a custom optuna sampler being honored. +* FLAML-option parity on the ``framework="flaml"`` path: ``cost_attr``, + ``low_cost_partial_config``, ``points_to_evaluate`` (from ``try_init_configs``, + possibly ``[]``) and ``evaluated_rewards`` (``[]`` on a fresh run, rebuilt from + the *previous* tuner's results on ``resume=True``) actually reaching + ``flaml.tune.run``. +* Best-effort behaviour elsewhere: ``resume=True`` raises ``NotImplementedError`` + on optuna/hyperopt, while the constructor-default ``try_init_configs`` only warns. +* The packaging decision (hiertunehub + optuna hard deps; hyperopt an extra). +""" + +import pathlib + +import pytest + +from causaltune import CausalTune +from causaltune.datasets import synth_ihdp +from causaltune.search.params import SimpleParamService +from causaltune.score.scoring import Scorer + +# A single cheap estimator keeps the end-to-end fits fast. LinearDML carries a +# small tunable search space, which every backend (incl. hyperopt) can handle -- +# hiertunehub 0.2.1's to_hyperopt() NameErrors on an all-parameterless choice. +CHEAP_ESTIMATORS = ["LinearDML"] + + +@pytest.fixture(scope="module") +def data(): + cd = synth_ihdp() + cd.preprocess_dataset() + return cd + + +def _make_ct(**overrides): + kwargs = dict( + metric="energy_distance", + estimator_list=CHEAP_ESTIMATORS, + num_samples=2, + components_time_budget=3, + use_ray=False, + ) + kwargs.update(overrides) + return CausalTune(**kwargs) + + +def _base_tuner_settings(**overrides): + settings = { + "num_samples": 5, + "time_budget_s": 10, + "verbose": 1, + "resources_per_trial": {"cpu": 0.5}, + "algo": None, + } + settings.update(overrides) + return settings + + +# --------------------------------------------------------------------------- # +# parse_tuner_params +# --------------------------------------------------------------------------- # +def test_parse_tuner_params_flaml(): + out = SimpleParamService.parse_tuner_params(_base_tuner_settings(), "flaml") + assert out["num_samples"] == 5 + assert out["time_budget_s"] == 10 + assert out["verbose"] == 1 + assert out["resources_per_trial"] == {"cpu": 0.5} + assert out["search_alg"] is None + + +def test_parse_tuner_params_optuna(): + out = SimpleParamService.parse_tuner_params(_base_tuner_settings(), "optuna") + assert out["n_trials"] == 5 + assert out["timeout"] == 10 + assert out["sampler"] is None + + +def test_parse_tuner_params_optuna_keys_exact(): + # optuna must not receive FLAML-only kwargs (search_alg/resources_per_trial/verbose) + out = SimpleParamService.parse_tuner_params(_base_tuner_settings(), "optuna") + assert set(out) == {"n_trials", "timeout", "sampler"} + + +def test_parse_tuner_params_optuna_num_samples_minus_one_is_none(): + out = SimpleParamService.parse_tuner_params( + _base_tuner_settings(num_samples=-1), "optuna" + ) + assert out["n_trials"] is None + # still bounded by the time budget, so no error + assert out["timeout"] == 10 + + +def test_parse_tuner_params_optuna_custom_sampler_passthrough(): + optuna = pytest.importorskip("optuna") + sampler = optuna.samplers.RandomSampler(seed=0) + out = SimpleParamService.parse_tuner_params( + _base_tuner_settings(algo=sampler), "optuna" + ) + assert out["sampler"] is sampler + + +def test_parse_tuner_params_hyperopt(): + hyperopt = pytest.importorskip("hyperopt") + out = SimpleParamService.parse_tuner_params(_base_tuner_settings(), "hyperopt") + assert out["max_evals"] == 5 + assert out["timeout"] == 10 + # algo defaults to TPE when the user passes None + assert out["algo"] is hyperopt.tpe.suggest + + +def test_parse_tuner_params_hyperopt_keys_exact(): + pytest.importorskip("hyperopt") + out = SimpleParamService.parse_tuner_params(_base_tuner_settings(), "hyperopt") + assert set(out) == {"max_evals", "timeout", "verbose", "algo"} + + +def test_parse_tuner_params_hyperopt_num_samples_minus_one_is_none(): + pytest.importorskip("hyperopt") + out = SimpleParamService.parse_tuner_params( + _base_tuner_settings(num_samples=-1), "hyperopt" + ) + assert out["max_evals"] is None + + +def test_parse_tuner_params_hyperopt_custom_algo_passthrough(): + hyperopt = pytest.importorskip("hyperopt") + out = SimpleParamService.parse_tuner_params( + _base_tuner_settings(algo=hyperopt.rand.suggest), "hyperopt" + ) + assert out["algo"] is hyperopt.rand.suggest + + +def test_parse_tuner_params_unsupported_framework(): + with pytest.raises(ValueError): + SimpleParamService.parse_tuner_params(_base_tuner_settings(), "nope") + + +def test_parse_tuner_params_optuna_unbounded_raises(): + # both n_trials (num_samples == -1) and timeout (time_budget_s is None) unset + with pytest.raises(ValueError): + SimpleParamService.parse_tuner_params( + _base_tuner_settings(num_samples=-1, time_budget_s=None), "optuna" + ) + + +def test_parse_tuner_params_hyperopt_unbounded_raises(): + pytest.importorskip("hyperopt") + with pytest.raises(ValueError): + SimpleParamService.parse_tuner_params( + _base_tuner_settings(num_samples=-1, time_budget_s=None), "hyperopt" + ) + + +def test_parse_tuner_params_flaml_unbounded_ok(): + # FLAML tolerates num_samples == -1 with no time budget; must NOT raise. + out = SimpleParamService.parse_tuner_params( + _base_tuner_settings(num_samples=-1, time_budget_s=None), "flaml" + ) + assert out["num_samples"] == -1 + + +# --------------------------------------------------------------------------- # +# search_space -> hiertunehub.SearchSpace +# --------------------------------------------------------------------------- # +def test_search_space_returns_hiertunehub_searchspace(): + SearchSpace = pytest.importorskip("hiertunehub").SearchSpace + cfg = SimpleParamService(n_jobs=1, include_experimental=False, multivalue=False) + est_list = cfg.estimator_names_from_patterns( + "backdoor", ["SLearner", "TLearner"], 1000 + ) + ss = cfg.search_space(est_list, data_size=(1000, 5)) + assert isinstance(ss, SearchSpace) + + flaml_space = ss.to_flaml() + assert "estimator" in flaml_space + categories = flaml_space["estimator"].categories + names = {c["estimator_name"] for c in categories} + assert names == set(est_list) + + +# --------------------------------------------------------------------------- # +# Scorer.best_score_by_estimator now takes a list +# --------------------------------------------------------------------------- # +def test_best_score_by_estimator_accepts_list(): + scores = [ + {"estimator_name": "A", "energy_distance": 1.0}, + {"estimator_name": "A", "energy_distance": 0.5}, + {"estimator_name": "B", "energy_distance": 2.0}, + ] + best = Scorer.best_score_by_estimator(scores, "energy_distance") + assert set(best) == {"A", "B"} + # energy_distance is minimized + assert best["A"]["energy_distance"] == 0.5 + assert best["B"]["energy_distance"] == 2.0 + + +def test_best_score_by_estimator_malformed_raises_valueerror(): + # Regression: the message must not reference the removed loop var `k` + # (which used to raise NameError instead of the intended ValueError). + with pytest.raises(ValueError): + Scorer.best_score_by_estimator([{"energy_distance": 1.0}], "energy_distance") + + +# --------------------------------------------------------------------------- # +# End-to-end fits across backends +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("framework", ["flaml", "optuna", "hyperopt"]) +def test_fit_end_to_end_per_backend(framework, data): + if framework == "hyperopt": + pytest.importorskip("hyperopt") + ct = _make_ct() + ct.fit(data, framework=framework) + + assert ct.tuner is not None + # the tuner must be a real hiertunehub Tuner (proves create_tuner is used, + # not a lingering direct flaml.tune.run path) + assert type(ct.tuner).__module__.startswith("hiertunehub") + assert ct.tuner.framework == framework + + # public properties are repointed to the tuner (not the legacy self.results) + assert ct.best_estimator == ct.tuner.best_result["estimator_name"] + assert ct.best_config == ct.tuner.best_params + assert ct.best_score == ct.tuner.best_result[ct.metric] + + assert isinstance(ct.best_estimator, str) + assert isinstance(ct.best_config, dict) + assert len(ct.scores) > 0 + + effect = ct.effect(data.data) + assert len(effect) == len(data.data) + + +def test_default_framework_is_optuna(data): + pytest.importorskip("optuna") + ct = _make_ct() + ct.fit(data) # no framework -> new default + assert ct.tuner.framework == "optuna" + + +def test_optuna_custom_sampler_is_honored(data): + optuna = pytest.importorskip("optuna") + ct = _make_ct() + ct.fit(data, framework="optuna", algo=optuna.samplers.RandomSampler(seed=0)) + assert isinstance(ct.tuner.study.sampler, optuna.samplers.RandomSampler) + + +@pytest.mark.parametrize("store_all", [False, True]) +def test_model_and_effect_after_fit(store_all, data): + ct = _make_ct(store_all_estimators=store_all) + ct.fit(data, framework="optuna") + assert isinstance(ct.best_estimator, str) + # `.model` must resolve the fitted estimator from ct.scores even though the + # objective pops the estimator object out of the result dict when not + # store_all -- so it can't come from tuner.best_result["estimator"]. + assert ct.model is not None + assert ct.model is ct.scores[ct.best_estimator]["estimator"].estimator + effect = ct.effect(data.data) + assert len(effect) == len(data.data) + + +@pytest.mark.parametrize("framework", ["flaml", "optuna"]) +def test_failed_trial_not_selected_as_best_for_minimize(monkeypatch, data, framework): + # A failed evaluation must not be picked as best. energy_distance is + # minimized, so the failure sentinel must be +inf (worst), never -inf -- a + # -inf would look optimal to a minimizing backend and poison selection. + import numpy as np + + orig_stub = CausalTune._est_effect_stub + state = {"n": 0} + + def flaky_stub(self, method_params): + state["n"] += 1 + if state["n"] == 1: # force the first trial to fail + raise RuntimeError("boom") + return orig_stub(self, method_params) + + monkeypatch.setattr(CausalTune, "_est_effect_stub", flaky_stub) + + ct = _make_ct() # metric="energy_distance" (minimized), num_samples=2 + ct.fit(data, framework=framework) + + assert state["n"] >= 2 # at least one forced failure plus a real trial + # best must be the finite (successful) trial, not the +inf failure + assert np.isfinite(ct.best_score) + assert ct.model is not None + + +# --------------------------------------------------------------------------- # +# FLAML-option parity (framework="flaml" only) +# --------------------------------------------------------------------------- # +def _spy_flaml_run(monkeypatch, sink): + """Patch flaml.tune.run to record kwargs (into ``sink``) then call through.""" + import flaml.tune + + real_run = flaml.tune.run + + def spy(*args, **kwargs): + # FLAML drains points_to_evaluate/evaluated_rewards in place as it runs, + # so snapshot them (shallow copy) BEFORE handing off to the real call. + snap = dict(kwargs) + for key in ("points_to_evaluate", "evaluated_rewards"): + if isinstance(snap.get(key), list): + snap[key] = list(snap[key]) + sink.append(snap) + return real_run(*args, **kwargs) + + monkeypatch.setattr(flaml.tune, "run", spy) + + +def _estimator_level_calls(calls): + """Filter the flaml.tune.run calls down to CausalTune's estimator-level + tuner call. The spy also captures the many component-model AutoML calls; + ours is the one carrying our custom ``cost_attr="evaluation_cost"``. + """ + return [c for c in calls if c.get("cost_attr") == "evaluation_cost"] + + +def test_flaml_receives_parity_params(monkeypatch, data): + calls = [] + _spy_flaml_run(monkeypatch, calls) + + ct = _make_ct(try_init_configs=True) + ct.fit(data, framework="flaml") + + ours = _estimator_level_calls(calls) + assert len(ours) == 1 + kw = ours[0] + assert kw.get("cost_attr") == "evaluation_cost" + assert kw.get("low_cost_partial_config") == {} + # try_init_configs=True must seed points_to_evaluate + assert len(kw.get("points_to_evaluate", [])) > 0 + # a fresh (non-resume) run has no prior rewards -> empty list, not None + assert kw.get("evaluated_rewards") == [] + + +def test_flaml_empty_init_configs_still_passed(monkeypatch, data): + calls = [] + _spy_flaml_run(monkeypatch, calls) + + ct = _make_ct(try_init_configs=False) + ct.fit(data, framework="flaml") + + ours = _estimator_level_calls(calls) + assert len(ours) == 1 + # points_to_evaluate is an empty list, not omitted / None + assert ours[0].get("points_to_evaluate") == [] + assert ours[0].get("evaluated_rewards") == [] + + +def test_flaml_resume_rebuilds_from_previous_tuner(monkeypatch, data): + calls = [] + _spy_flaml_run(monkeypatch, calls) + + # Resume is a time-budgeted "continue with more budget" flow, so drive the + # runs by time (num_samples == -1). Use the near-instant Dummy estimator so + # several trials actually complete within a small budget (a slow estimator + # can consume the whole budget on a single trial, leaving nothing to resume). + ct = CausalTune( + metric="energy_distance", + estimator_list=["Dummy"], + num_samples=-1, + components_time_budget=3, + use_ray=False, + try_init_configs=False, + ) + ct.fit(data, framework="flaml", time_budget=5) + + # what the first run actually produced (captured BEFORE the resume fit) + prev_results = ct.tuner.results + expected_rewards = [ + r[ct.metric] for r in prev_results if ct.metric in r and "config" in r + ] + expected_configs = [ + r["config"] for r in prev_results if ct.metric in r and "config" in r + ] + assert expected_rewards # sanity: the first run yielded resumable trials + + calls.clear() # drop the first fit's calls; keep only the resume fit's + ct.fit(data, framework="flaml", resume=True, time_budget=5) + + ours = _estimator_level_calls(calls) + assert len(ours) == 1 + resumed = ours[0] + # rewards are rebuilt exactly from the previous tuner's results... + assert resumed.get("evaluated_rewards") == expected_rewards + # ...and aligned to the leading points_to_evaluate configs + assert ( + resumed.get("points_to_evaluate", [])[: len(expected_configs)] + == expected_configs + ) + + +def test_resume_rebuild_skips_malformed_and_aligns_rewards(): + ct = _make_ct() + ct.metric = "energy_distance" + prev_results = [ + {"estimator_name": "A", "energy_distance": 1.0, "config": {"a": 1}}, + {"estimator_name": "A", "config": {"a": 2}}, # missing metric -> skip + {"estimator_name": "A", "energy_distance": 3.0}, # missing config -> skip + ] + cfg, scores = ct._resume_points_and_rewards(prev_results, init_cfg=[{"a": 9}]) + # only the first (fully-formed) result contributes a reward + assert scores == [1.0] + # its config leads, and the unevaluated init config is appended after + assert cfg[: len(scores)] == [{"a": 1}] + assert {"a": 9} in cfg + # rewards align to the leading configs; never more rewards than configs + assert len(scores) <= len(cfg) + + +# --------------------------------------------------------------------------- # +# Best-effort behaviour on non-flaml backends +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("framework", ["optuna", "hyperopt"]) +def test_resume_raises_notimplemented_on_non_flaml(framework, data): + if framework == "hyperopt": + pytest.importorskip("hyperopt") + ct = _make_ct() + with pytest.raises(NotImplementedError): + ct.fit(data, framework=framework, resume=True) + + +@pytest.mark.parametrize("framework", ["optuna", "hyperopt"]) +def test_default_try_init_configs_warns_on_non_flaml(framework, data): + if framework == "hyperopt": + pytest.importorskip("hyperopt") + # _make_ct() leaves try_init_configs at its constructor default (True) + ct = _make_ct() + with pytest.warns(UserWarning, match="init config"): + ct.fit(data, framework=framework) + assert ct.tuner is not None + + +def test_hyperopt_parameterless_search_raises_clear_error(data): + pytest.importorskip("hyperopt") + # Dummy is parameterless and outcome_model defaults to "nested" (not auto), + # so the hyperopt search space has no tunable params. Guard with a clear + # ValueError instead of a cryptic hiertunehub NameError. + ct = CausalTune( + metric="energy_distance", + estimator_list=["Dummy"], + num_samples=2, + components_time_budget=3, + use_ray=False, + ) + with pytest.raises(ValueError, match="hyperopt"): + ct.fit(data, framework="hyperopt") + + +# --------------------------------------------------------------------------- # +# Packaging decision +# --------------------------------------------------------------------------- # +def test_packaging_dependencies(): + # tomllib is stdlib only from 3.11; the metadata it checks is + # Python-independent, so just skip on 3.10 rather than pulling in tomli. + tomllib = pytest.importorskip("tomllib") + + root = pathlib.Path(__file__).resolve().parents[2] + pyproject = tomllib.loads((root / "pyproject.toml").read_text()) + + deps = " ".join(pyproject["project"]["dependencies"]) + assert "hiertunehub" in deps + assert "optuna" in deps # default backend -> hard dep + + extras = pyproject["project"].get("optional-dependencies", {}) + hyperopt_extra = " ".join(extras.get("hyperopt", [])) + assert "hyperopt" in hyperopt_extra # optional extra, not a core dep + assert "setuptools" in hyperopt_extra # pkg_resources pin lives with hyperopt