16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 | class SklearnCalculator(BaseModelCalculator):
"""Base calculator for Scikit-Learn models."""
def __init__(
self,
model_class: type[BaseEstimator],
default_params: dict[str, Any],
problem_type: str,
):
# `Any` because sklearn stubs make BaseEstimator subclasses appear non-callable.
self.model_class: Any = model_class
self._default_params = default_params
self._problem_type = problem_type
@property
def default_params(self) -> dict[str, Any]:
return self._default_params
@property
def problem_type(self) -> str:
return self._problem_type
def fit(
self,
X: pd.DataFrame | SkyulfDataFrame,
y: pd.Series | Any,
config: dict[str, Any],
progress_callback=None,
log_callback=None,
validation_data=None,
) -> Any:
"""Fit the Scikit-Learn model."""
# 1. Merge Config with Defaults
params = self._resolve_fit_params(config)
msg = f"Initializing {self.model_class.__name__} with params: {params}"
logger.info(msg)
if log_callback:
log_callback(msg)
# 2. Instantiate Model
valid_params = self._filter_supported_params(params)
model = self.model_class(**valid_params)
# 3. Fit
# Convert to Numpy using Bridge (handles Polars/Pandas/Wrappers)
X_np, y_np = SklearnBridge.to_sklearn((X, y))
model.fit(X_np, y_np)
return model
def _resolve_fit_params(self, config: dict[str, Any]) -> dict[str, Any]:
"""Merges ``default_params`` with overrides from ``config``.
Supports two configuration structures: a nested ``{'params': {...}}`` dict
(preferred), or a flat legacy dict where non-reserved, non-dict keys are
treated as params.
"""
params = self.default_params.copy()
if not config:
return params
# We support two configuration structures:
# 1. Nested: {'params': {'C': 1.0, ...}} - Preferred
# 2. Flat: {'C': 1.0, 'type': '...', ...} - Legacy/Simple support
# Check for explicit 'params' dictionary first
overrides = config.get("params", {})
# If 'params' key exists but is None or empty, check if there are other keys at top level
# that might be params. But be careful not to mix them.
# If config has 'params', we assume it's the source of truth.
if not overrides and "params" not in config:
# Fallback to flat config if 'params' key is completely missing
reserved_keys = {
"type",
"target_column",
"node_id",
"step_type",
"inputs",
}
overrides = {
k: v
for k, v in config.items()
if k not in reserved_keys and not isinstance(v, dict)
}
if overrides:
params.update(overrides)
return params
def _filter_supported_params(self, params: dict[str, Any]) -> dict[str, Any]:
"""Filters ``params`` down to those accepted by the model class constructor.
Skips filtering when the constructor accepts ``**kwargs`` (e.g. XGBoost 2.x),
since every named param would otherwise fail the membership check even though valid.
"""
import inspect
sig = inspect.signature(self.model_class)
accepts_kwargs = any(
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
)
if accepts_kwargs:
return params
valid_params = {k: v for k, v in params.items() if k in sig.parameters}
dropped = set(params.keys()) - set(valid_params.keys())
if dropped:
logger.warning(
f"Dropped parameters not supported by {self.model_class.__name__}: {dropped}"
)
return valid_params
|