Skip to content

Commit 04f2a0d

Browse files
authored
Parameter validation with pydantic (#16)
* bugfixing * bump version * cleanup
1 parent f4468e2 commit 04f2a0d

18 files changed

Lines changed: 153 additions & 101 deletions

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ install_requires =
5252
miracle-imputation
5353
catboost
5454
xgboost
55+
pydantic
5556
importlib-metadata; python_version<"3.8"
5657

5758

src/hyperimpute/plugins/core/base_plugin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# third party
1010
from optuna.trial import Trial
1111
import pandas as pd
12+
from pydantic import validate_arguments
1213

1314
# hyperimpute absolute
1415
import hyperimpute.logger as log
@@ -110,20 +111,24 @@ def subtype() -> str:
110111
def fqdn(cls) -> str:
111112
return cls.type() + "." + cls.subtype() + "." + cls.name()
112113

114+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
113115
def fit_transform(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFrame:
114116
return pd.DataFrame(self.fit(X, *args, *kwargs).transform(X))
115117

118+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
116119
def fit_predict(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFrame:
117120
return pd.DataFrame(self.fit(X, *args, *kwargs).predict(X))
118121

119-
def fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> "Plugin":
122+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
123+
def fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> Any:
120124
X = cast.to_dataframe(X)
121125
return self._fit(X, *args, **kwargs)
122126

123127
@abstractmethod
124128
def _fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> "Plugin":
125129
...
126130

131+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
127132
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
128133
X = cast.to_dataframe(X)
129134
return pd.DataFrame(self._transform(X))
@@ -132,6 +137,7 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame:
132137
def _transform(self, X: pd.DataFrame) -> pd.DataFrame:
133138
...
134139

140+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
135141
def predict(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFrame:
136142
X = cast.to_dataframe(X)
137143
return pd.DataFrame(self._predict(X, *args, *kwargs))

src/hyperimpute/plugins/core/params.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
# third party
66
import numpy as np
77
from optuna.trial import Trial
8+
from pydantic import validate_arguments
89

910

1011
class Params(metaclass=ABCMeta):
12+
@validate_arguments
1113
def __init__(self, name: str, bounds: Tuple[Any, Any]) -> None:
1214
self.name = name
1315
self.bounds = bounds
@@ -26,6 +28,7 @@ def sample_np(self) -> Any:
2628

2729

2830
class Categorical(Params):
31+
@validate_arguments
2932
def __init__(self, name: str, choices: List[Any]) -> None:
3033
super().__init__(name, (min(choices), max(choices)))
3134
self.name = name
@@ -42,6 +45,7 @@ def sample_np(self) -> Any:
4245

4346

4447
class Float(Params):
48+
@validate_arguments
4549
def __init__(self, name: str, low: float, high: float) -> None:
4650
low = float(low)
4751
high = float(high)
@@ -54,6 +58,7 @@ def __init__(self, name: str, low: float, high: float) -> None:
5458
def get(self) -> List[Any]:
5559
return [self.name, self.low, self.high]
5660

61+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
5762
def sample(self, trial: Trial) -> float:
5863
return trial.suggest_float(self.name, self.low, self.high)
5964

@@ -62,6 +67,7 @@ def sample_np(self) -> Any:
6267

6368

6469
class Integer(Params):
70+
@validate_arguments
6571
def __init__(self, name: str, low: int, high: int, step: int = 1) -> None:
6672
self.low = low
6773
self.high = high
@@ -77,6 +83,7 @@ def __init__(self, name: str, low: int, high: int, step: int = 1) -> None:
7783
def get(self) -> List[Any]:
7884
return [self.name, self.low, self.high, self.step]
7985

86+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
8087
def sample(self, trial: Trial) -> Any:
8188
return trial.suggest_int(self.name, self.low, self.high, self.step)
8289

src/hyperimpute/plugins/imputers/plugin_hyperimpute.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import numpy as np
1010
import optuna
1111
import pandas as pd
12+
from pydantic import validate_arguments
1213
from sklearn.impute import MissingIndicator
1314
from sklearn.metrics import mean_squared_error
1415
from sklearn.preprocessing import LabelEncoder
@@ -61,6 +62,7 @@ def default(self, obj: Any) -> Any:
6162

6263

6364
class HyperbandOptimizer:
65+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
6466
def __init__(
6567
self,
6668
name: str,
@@ -112,6 +114,7 @@ def _hash_dict(self, name: str, dict_val: dict) -> str:
112114
{"name": name, "val": dict_val}, sort_keys=True, cls=NpEncoder
113115
)
114116

117+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
115118
def _sample_model(self, name: str, n: int) -> list:
116119
hashed = self._hash_dict(name, {})
117120
result: List[Tuple] = []
@@ -132,19 +135,22 @@ def _sample_model(self, name: str, n: int) -> list:
132135

133136
return result
134137

138+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
135139
def _sample(self, n: int) -> list:
136140
results = []
137141
for name in self.seeds:
138142
results.extend(self._sample_model(name, n))
139143
return results
140144

141-
def _baseline(self, X: pd.DataFrame, y: pd.DataFrame) -> None:
145+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
146+
def _baseline(self, X: pd.DataFrame, y: pd.Series) -> None:
142147
for seed in self.seeds:
143148
self._eval_params(seed, X, y, hyperparam_search_iterations=1)
144149
# TODO: balance methods
145150

151+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
146152
def _eval_params(
147-
self, model_name: str, X: pd.DataFrame, y: pd.DataFrame, **params: Any
153+
self, model_name: str, X: pd.DataFrame, y: pd.Series, **params: Any
148154
) -> float:
149155
model = self.predictions.get(model_name, **params)
150156
for n_folds in [2, 1]:
@@ -171,9 +177,8 @@ def _eval_params(
171177

172178
return score
173179

174-
def evaluate(
175-
self, X: pd.DataFrame, y: pd.DataFrame
176-
) -> Tuple[PredictionPlugin, float]:
180+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
181+
def evaluate(self, X: pd.DataFrame, y: pd.Series) -> Tuple[PredictionPlugin, float]:
177182
self._reset()
178183
self._baseline(X, y)
179184

@@ -236,6 +241,7 @@ def evaluate(
236241

237242

238243
class BayesianOptimizer:
244+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
239245
def __init__(
240246
self,
241247
name: str,
@@ -272,11 +278,12 @@ def __init__(
272278
self.best_candidate = self.seeds[0]
273279
self.best_params: dict = {}
274280

281+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
275282
def evaluate_plugin(
276283
self,
277284
plugin_name: str,
278285
X: pd.DataFrame,
279-
y: pd.DataFrame,
286+
y: pd.Series,
280287
prev_best_score: float,
281288
) -> tuple:
282289
# BO evaluation for a single plugin
@@ -333,8 +340,9 @@ def objective(trial: optuna.Trial) -> float:
333340

334341
return study.best_value, study.best_trial.params
335342

343+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
336344
def evaluate(
337-
self, X_train: pd.DataFrame, y_train: pd.DataFrame
345+
self, X_train: pd.DataFrame, y_train: pd.Series
338346
) -> Tuple[PredictionPlugin, float]:
339347
best_score = self.failure_score
340348

@@ -366,6 +374,7 @@ def evaluate(
366374

367375

368376
class SimpleOptimizer:
377+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
369378
def __init__(
370379
self,
371380
name: str,
@@ -398,8 +407,9 @@ def __init__(
398407
for seed in self.seeds:
399408
self.model_best_score[seed] = -np.inf
400409

410+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
401411
def _eval_params(
402-
self, model_name: str, X: pd.DataFrame, y: pd.DataFrame, **params: Any
412+
self, model_name: str, X: pd.DataFrame, y: pd.Series, **params: Any
403413
) -> float:
404414
model = self.predictions.get(
405415
model_name, random_state=self.random_state, **params
@@ -430,9 +440,8 @@ def _eval_params(
430440

431441
return score
432442

433-
def evaluate(
434-
self, X: pd.DataFrame, y: pd.DataFrame
435-
) -> Tuple[PredictionPlugin, float]:
443+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
444+
def evaluate(self, X: pd.DataFrame, y: pd.Series) -> Tuple[PredictionPlugin, float]:
436445
for seed in self.seeds:
437446
self._eval_params(seed, X, y)
438447
log.info(
@@ -447,6 +456,7 @@ def evaluate(
447456

448457

449458
class IterativeErrorCorrection:
459+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
450460
def __init__(
451461
self,
452462
study: str,
@@ -530,10 +540,12 @@ def intersect_or_right(left: list, right: list) -> list:
530540
"regression_seed": reg,
531541
}
532542

543+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
533544
def _setup(self, X: pd.DataFrame) -> pd.DataFrame:
534545
# Encode the categorical columns
535546
# Reset internal caches
536547
X = pd.DataFrame(X).copy()
548+
X.columns = X.columns.map(str)
537549

538550
self.mask = self._missing_indicator(X)
539551

@@ -590,25 +602,29 @@ def _setup(self, X: pd.DataFrame) -> pd.DataFrame:
590602

591603
return X
592604

605+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
593606
def _tear_down(self, X: pd.DataFrame) -> pd.DataFrame:
594607
# Revert the encoding after processing the data
595608
for col in self.encoders:
596609
X[col] = self.encoders[col].inverse_transform(X[col].astype(int))
597610

598611
return X
599612

613+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
600614
def _get_neighbors_for_col(self, col: str) -> list:
601615
covs = list(self.all_cols)
602616
covs.remove(col)
603617

604618
return covs
605619

620+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
606621
def _is_same_type(self, lhs: str, rhs: str) -> bool:
607622
ltype = lhs in self.categorical_cols
608623
rtype = rhs in self.categorical_cols
609624

610625
return ltype == rtype
611626

627+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
612628
def _check_similar(self, X: pd.DataFrame, col: str) -> Any:
613629
if not self.select_lazy:
614630
return None
@@ -630,6 +646,7 @@ def _check_similar(self, X: pd.DataFrame, col: str) -> Any:
630646

631647
return None
632648

649+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
633650
def _optimize_model_for_column(self, X: pd.DataFrame, col: str) -> float:
634651
# BO evaluation for a single column
635652
if self.mask[col].sum() == 0:
@@ -666,6 +683,7 @@ def _optimize_model_for_column(self, X: pd.DataFrame, col: str) -> float:
666683

667684
return score
668685

686+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
669687
def _optimize(self, X: pd.DataFrame) -> float:
670688
# BO evaluation to select the best models for each columns
671689
if self.select_model_by_iteration:
@@ -677,6 +695,7 @@ def _optimize(self, X: pd.DataFrame) -> float:
677695

678696
return iteration_score
679697

698+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
680699
def _impute_single_column(
681700
self, X: pd.DataFrame, col: str, train: bool
682701
) -> pd.DataFrame:
@@ -723,14 +742,17 @@ def _get_imputation_order(self) -> list:
723742
random.shuffle(self.imputation_order)
724743
return self.imputation_order
725744

745+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
726746
def _initial_imputation(self, X: pd.DataFrame) -> pd.DataFrame:
727747
# Use baseline imputer for initial values
728748
return self.baseline_imputer.fit_transform(X)
729749

750+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
730751
def _is_categorical(self, X: pd.DataFrame, col: str) -> bool:
731752
# Helper for filtering categorical columns
732753
return len(X[col].unique()) < self.class_threshold
733754

755+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
734756
def _missing_indicator(self, X: pd.DataFrame) -> pd.DataFrame:
735757
# Helper for generating missingness mask
736758
return pd.DataFrame(
@@ -739,6 +761,7 @@ def _missing_indicator(self, X: pd.DataFrame) -> pd.DataFrame:
739761
index=X.index,
740762
)
741763

764+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
742765
def _fit_transform_inner_optimization(self, X: pd.DataFrame) -> pd.DataFrame:
743766
log.info(" > HyperImpute using inner optimization")
744767
best_obj_score = -10e10
@@ -780,6 +803,7 @@ def _fit_transform_inner_optimization(self, X: pd.DataFrame) -> pd.DataFrame:
780803

781804
return X
782805

806+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
783807
def fit_transform(self, X: pd.DataFrame) -> pd.DataFrame:
784808
# Run imputation
785809
X = self._setup(X)

src/hyperimpute/plugins/imputers/plugin_miracle.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ def _fit(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> "MiraclePlugin":
9595
return self
9696

9797
def _transform(self, X: pd.DataFrame) -> pd.DataFrame:
98-
X = np.asarray(X)
99-
missing_idxs = np.where(np.any(np.isnan(np.asarray(X)), axis=0))[0]
98+
missing_idxs = np.where(np.any(np.isnan(X.values), axis=0))[0]
10099

101100
_model = MIRACLE(
102101
num_inputs=X.shape[1],
@@ -117,7 +116,7 @@ def _transform(self, X: pd.DataFrame) -> pd.DataFrame:
117116
seed_imputer = self._get_seed_imputer(self.seed_imputation)
118117
X_seed = seed_imputer.fit_transform(X)
119118

120-
return _model.fit(X, X_seed=X_seed)
119+
return _model.fit(X.values, X_seed=X_seed.values)
121120

122121
def save(self) -> bytes:
123122
return b""

src/hyperimpute/plugins/prediction/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# third party
66
import pandas as pd
7+
from pydantic import validate_arguments
78

89
# hyperimpute absolute
910
import hyperimpute.plugins.core.base_plugin as plugin
@@ -37,12 +38,15 @@ def _transform(self, X: pd.DataFrame) -> pd.DataFrame:
3738
"Prediction plugins do not implement the 'transform' method"
3839
)
3940

41+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
4042
def score(self, X: pd.DataFrame, y: pd.DataFrame, metric: str = "aucroc") -> float:
4143
raise NotImplementedError(f"Score not implemented for {self.name()}")
4244

45+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
4346
def explain(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFrame:
4447
raise NotImplementedError(f"Explainer not implemented for {self.name()}")
4548

49+
@validate_arguments(config=dict(arbitrary_types_allowed=True))
4650
def predict_proba(self, X: pd.DataFrame, *args: Any, **kwargs: Any) -> pd.DataFrame:
4751
X = cast.to_dataframe(X)
4852
return pd.DataFrame(self._predict_proba(X, *args, **kwargs))

0 commit comments

Comments
 (0)