get_label_weighting_config,
get_min_max_label_period_candles,
get_optuna_study_model_parameters,
+ get_refit_model_training_parameters,
label_known_at_lookahead_column_name,
label_weight_column_name,
label_weight_known_at_lookahead_column_name,
and self.data_split_parameters.get(
"test_size", QuickAdapterRegressorV3._TEST_SIZE
)
- > 0
+ != 0
)
self._optuna_hp_value: dict[str, float] = {}
+ self._holdout_rmse: dict[str, float] = {}
self._optuna_label_values: dict[str, list[float | int]] = {}
self._optuna_hp_params: dict[str, dict[str, Any]] = {}
self._optuna_label_params: dict[str, dict[str, Any]] = {}
trade_mode = self.config.get("runmode") in TRADE_MODES
for pair in self.pairs:
self._optuna_hp_value[pair] = -1
+ self._holdout_rmse[pair] = np.inf
self._optuna_label_values[pair] = [
-1
] * QuickAdapterRegressorV3._OPTUNA_LABEL_N_OBJECTIVES
f"{end_date} --------------------"
)
dd = split_fn(features_filtered, labels_filtered, weights, unfiltered_df)
+ dd = self._add_validation_split(
+ dd, features_filtered, weights, unfiltered_df, pair
+ )
+ dd = self._add_refit_data(
+ dd,
+ features_filtered,
+ labels_filtered,
+ weights,
+ unfiltered_df,
+ pair,
+ )
if not self.freqai_info.get("fit_live_predictions_candles", 0) or not self.live:
dk.fit_labels()
dd = self._apply_pipelines(dd, dk, pair)
)
return model
+ def _known_before_position_mask(
+ self,
+ features: pd.DataFrame,
+ unfiltered_df: pd.DataFrame,
+ pair: str,
+ cutoff_position: int,
+ ) -> NDArray[np.bool_]:
+ """Return rows whose labels are known before ``cutoff_position``."""
+ row_positions = QuickAdapterRegressorV3._row_positions(features, unfiltered_df)
+ known_at_lookahead = QuickAdapterRegressorV3._known_at_lookahead(
+ features, unfiltered_df
+ )
+ if known_at_lookahead is None:
+ return (
+ row_positions.to_numpy(dtype=np.int64)
+ + self._label_horizon_candles(pair)
+ < cutoff_position
+ )
+ return (
+ row_positions.to_numpy(dtype=np.int64)
+ + known_at_lookahead.to_numpy(dtype=np.int64)
+ < cutoff_position
+ )
+
+ def _add_validation_split(
+ self,
+ data_dictionary: dict[str, Any],
+ features: pd.DataFrame,
+ weights: SampleWeightInputs,
+ unfiltered_df: pd.DataFrame,
+ pair: str,
+ ) -> dict[str, Any]:
+ """Reserve the chronological tail of the training set for selection."""
+ validation_size = self.data_split_parameters.get(
+ "test_size", QuickAdapterRegressorV3._TEST_SIZE
+ )
+ if validation_size is None:
+ validation_size = QuickAdapterRegressorV3._TEST_SIZE
+ if validation_size == 0:
+ data_dictionary["validation_features"] = data_dictionary[
+ "train_features"
+ ].iloc[:0]
+ data_dictionary["validation_labels"] = data_dictionary["train_labels"].iloc[
+ :0
+ ]
+ data_dictionary["validation_weights"] = data_dictionary["train_weights"][:0]
+ return data_dictionary
+ if (
+ self.data_split_parameters.get("shuffle", False)
+ or self.ft_params.get("shuffle_after_split", False)
+ or self.ft_params.get("reverse_train_test_order", False)
+ ):
+ raise ValueError(
+ "Independent holdout evaluation requires shuffle=false, "
+ "shuffle_after_split=false, and reverse_train_test_order=false"
+ )
+
+ n_train_rows = len(data_dictionary["train_features"])
+ if (
+ isinstance(validation_size, int)
+ and not isinstance(validation_size, bool)
+ and validation_size >= n_train_rows
+ ):
+ raise DependencyException(
+ f"[{pair}] inner validation count test_size={validation_size} is not "
+ f"smaller than the {n_train_rows} training rows remaining after the "
+ f"outer holdout; reduce test_size or provide more data"
+ )
+
+ (
+ train_features,
+ validation_features,
+ train_labels,
+ validation_labels,
+ ) = train_test_split(
+ data_dictionary["train_features"],
+ data_dictionary["train_labels"],
+ test_size=validation_size,
+ shuffle=False,
+ )
+
+ if self._causal_mode:
+ row_positions = QuickAdapterRegressorV3._row_positions(
+ data_dictionary["train_features"], unfiltered_df
+ )
+ first_validation_position = int(
+ row_positions.loc[validation_features.index].min()
+ )
+ train_positions = row_positions.loc[train_features.index]
+ known_at_lookahead = QuickAdapterRegressorV3._known_at_lookahead(
+ data_dictionary["train_features"], unfiltered_df
+ )
+ if known_at_lookahead is None:
+ keep_mask = train_positions.to_numpy(
+ dtype=np.int64
+ ) < first_validation_position - self._label_horizon_candles(pair)
+ else:
+ train_known_at_position = train_positions.to_numpy(
+ dtype=np.int64
+ ) + known_at_lookahead.loc[train_features.index].to_numpy(
+ dtype=np.int64
+ )
+ keep_mask = train_known_at_position < first_validation_position
+ train_features = train_features.loc[keep_mask]
+ train_labels = train_labels.loc[keep_mask]
+
+ holdout_features = data_dictionary["test_features"]
+ if not holdout_features.empty:
+ holdout_mask = self._known_before_position_mask(
+ holdout_features,
+ unfiltered_df,
+ pair,
+ len(unfiltered_df),
+ )
+ data_dictionary["test_features"] = holdout_features.loc[holdout_mask]
+ data_dictionary["test_labels"] = data_dictionary["test_labels"].loc[
+ holdout_mask
+ ]
+ data_dictionary["test_weights"] = data_dictionary["test_weights"][
+ holdout_mask
+ ]
+ if data_dictionary["test_features"].empty:
+ logger.warning(
+ f"[{pair}] causal purge emptied the holdout (label horizon "
+ f">= holdout span); skipping holdout evaluation "
+ f"(holdout_rmse=inf)"
+ )
+ data_dictionary["holdout_purged_empty"] = True
+
+ if train_features.empty or validation_features.empty:
+ raise DependencyException(
+ f"[{pair}] train or validation set is empty after chronological split"
+ )
+
+ # Recompose weights on the ACTUAL inner-train/validation rows instead of
+ # slicing the outer-train composition: a slice bypasses support_policy and
+ # lets sanitize_and_renormalize silently uniformize a pivot-sparse inner
+ # split. Realign raw weight components to the split rows by index. The
+ # recompose renormalizes each subset to mean 1 (proportional to, not
+ # byte-identical with, the old slice), which is scale-invariant for the
+ # weighted-RMSE selection and consistent with the refit weight scale.
+ train_positions = features.index.get_indexer(train_features.index)
+ validation_positions = features.index.get_indexer(validation_features.index)
+ if (train_positions < 0).any() or (validation_positions < 0).any():
+ raise ValueError(
+ f"[{pair}] _add_validation_split: unable to align split rows to "
+ f"sample weight inputs (missing train="
+ f"{int((train_positions < 0).sum())}, validation="
+ f"{int((validation_positions < 0).sum())})"
+ )
+ train_weights = QuickAdapterRegressorV3._compose_train_weights_with_support(
+ weights.base[train_positions],
+ None if weights.label is None else weights.label[train_positions],
+ weights.label_weighting_config,
+ context=f"[{pair}] validation_split:train",
+ )
+ validation_weights = QuickAdapterRegressorV3._compose_eval_weights(
+ weights.base[validation_positions],
+ None if weights.label is None else weights.label[validation_positions],
+ context=f"[{pair}] validation_split:validation",
+ )
+
+ data_dictionary["train_features"] = train_features
+ data_dictionary["train_labels"] = train_labels
+ data_dictionary["train_weights"] = train_weights
+ data_dictionary["validation_features"] = validation_features
+ data_dictionary["validation_labels"] = validation_labels
+ data_dictionary["validation_weights"] = validation_weights
+ return data_dictionary
+
+ def _add_refit_data(
+ self,
+ data_dictionary: dict[str, Any],
+ features: pd.DataFrame,
+ labels: pd.DataFrame,
+ weights: SampleWeightInputs,
+ unfiltered_df: pd.DataFrame,
+ pair: str,
+ ) -> dict[str, Any]:
+ """Keep all causally available rows for the final post-holdout refit."""
+ validation_size = self.data_split_parameters.get(
+ "test_size", QuickAdapterRegressorV3._TEST_SIZE
+ )
+ if validation_size is None:
+ validation_size = QuickAdapterRegressorV3._TEST_SIZE
+ if validation_size == 0:
+ return data_dictionary
+
+ keep_mask = np.ones(len(features), dtype=bool)
+ if self._causal_mode:
+ keep_mask = self._known_before_position_mask(
+ features,
+ unfiltered_df,
+ pair,
+ len(unfiltered_df),
+ )
+
+ refit_features = features.loc[keep_mask]
+ refit_labels = labels.loc[keep_mask]
+ refit_base_weights = weights.base[keep_mask]
+ refit_label_weights = (
+ None if weights.label is None else weights.label[keep_mask]
+ )
+ if (
+ self.data_split_parameters.get(
+ "method", QuickAdapterRegressorV3.DATA_SPLIT_METHOD_DEFAULT
+ )
+ == QuickAdapterRegressorV3._DATA_SPLIT_METHODS[1]
+ ):
+ max_train_size = QuickAdapterRegressorV3._coerce_optional_int(
+ self.data_split_parameters.get(
+ "max_train_size",
+ QuickAdapterRegressorV3.TIMESERIES_MAX_TRAIN_SIZE_DEFAULT,
+ ),
+ "max_train_size",
+ minimum=1,
+ )
+ if max_train_size is not None:
+ refit_features = refit_features.iloc[-max_train_size:]
+ refit_labels = refit_labels.iloc[-max_train_size:]
+ refit_base_weights = refit_base_weights[-max_train_size:]
+ if refit_label_weights is not None:
+ refit_label_weights = refit_label_weights[-max_train_size:]
+ if refit_features.empty:
+ raise DependencyException(
+ f"[{pair}] final refit set is empty after causal availability filtering"
+ )
+
+ data_dictionary["refit_features"] = refit_features
+ data_dictionary["refit_labels"] = refit_labels
+ data_dictionary["refit_weights"] = (
+ QuickAdapterRegressorV3._compose_train_weights_with_support(
+ refit_base_weights,
+ refit_label_weights,
+ weights.label_weighting_config,
+ context=f"[{pair}] refit",
+ )
+ )
+ return data_dictionary
+
+ def _fit_training_pipelines(
+ self,
+ features: pd.DataFrame,
+ labels: pd.DataFrame,
+ weights: NDArray[np.floating],
+ dk: FreqaiDataKitchen,
+ pair: str,
+ context: str,
+ ) -> tuple[pd.DataFrame, pd.DataFrame, NDArray[np.floating]]:
+ """Fit FreqAI pipelines and return their transformed training data."""
+ dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
+ dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
+ features, labels, weights = dk.feature_pipeline.fit_transform(
+ features, labels, weights
+ )
+ weights = sanitize_and_renormalize(
+ weights,
+ logger=logger,
+ context=f"[{pair}] post_feature_pipeline:{context}",
+ )
+ labels, _, _ = dk.label_pipeline.fit_transform(labels)
+ return features, labels, weights
+
def _apply_pipelines(
self,
dd: dict,
pair: str,
) -> dict:
"""Apply feature and label pipelines; renormalize weights post-transform."""
- dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
- dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
-
(dd["train_features"], dd["train_labels"], dd["train_weights"]) = (
- dk.feature_pipeline.fit_transform(
- dd["train_features"], dd["train_labels"], dd["train_weights"]
+ self._fit_training_pipelines(
+ dd["train_features"],
+ dd["train_labels"],
+ dd["train_weights"],
+ dk,
+ pair,
+ "train",
)
)
- dd["train_weights"] = sanitize_and_renormalize(
- dd["train_weights"],
- logger=logger,
- context=f"[{pair}] post_feature_pipeline:train",
- )
- dd["train_labels"], _, _ = dk.label_pipeline.fit_transform(dd["train_labels"])
+
+ if not dd["validation_features"].empty:
+ (
+ dd["validation_features"],
+ dd["validation_labels"],
+ dd["validation_weights"],
+ ) = dk.feature_pipeline.transform(
+ dd["validation_features"],
+ dd["validation_labels"],
+ dd["validation_weights"],
+ )
+ if dd["validation_features"].empty:
+ raise DependencyException(
+ f"[{pair}] validation set is empty after feature pipeline "
+ f"transform (outlier removal); relax SVM/DBSCAN outlier "
+ f"thresholds or increase test_size"
+ )
+ dd["validation_weights"] = sanitize_and_renormalize(
+ dd["validation_weights"],
+ logger=logger,
+ context=f"[{pair}] post_feature_pipeline:validation",
+ )
+ dd["validation_labels"], _, _ = dk.label_pipeline.transform(
+ dd["validation_labels"]
+ )
if (
self.data_split_parameters.get(
!= 0
):
if dd["test_labels"].shape[0] == 0:
+ if dd.get("holdout_purged_empty"):
+ return dd
method = self.data_split_parameters.get(
"method", QuickAdapterRegressorV3.DATA_SPLIT_METHOD_DEFAULT
)
X_test = data_dictionary.get("test_features")
y_test = data_dictionary.get("test_labels")
test_weights = data_dictionary.get("test_weights")
+ X_validation = data_dictionary.get("validation_features")
+ y_validation = data_dictionary.get("validation_labels")
+ validation_weights = data_dictionary.get("validation_weights")
+ validation_size = self.data_split_parameters.get(
+ "test_size", QuickAdapterRegressorV3._TEST_SIZE
+ )
+ if validation_size is None:
+ validation_size = QuickAdapterRegressorV3._TEST_SIZE
model_training_parameters = copy.deepcopy(self.model_training_parameters)
+ init_model = self.get_init_model(dk.pair)
start_time = time.time()
if self._optuna_hyperopt:
X,
y,
train_weights,
- X_test,
- y_test,
- test_weights,
- self.data_split_parameters.get(
- "test_size", QuickAdapterRegressorV3._TEST_SIZE
- ),
+ X_validation,
+ y_validation,
+ validation_weights,
+ validation_size,
self.get_optuna_params(dk.pair, _OPTUNA_NAMESPACES.hp),
model_training_parameters,
self._optuna_config.get(
QuickAdapterRegressorV3.OPTUNA_SPACE_FRACTION_DEFAULT,
),
dk.data_path,
+ init_model,
),
direction=optuna.study.StudyDirection.MINIMIZE,
)
}
eval_set, eval_weights = make_test_set_and_weights(
- X_test,
- y_test,
- test_weights,
- self.data_split_parameters.get(
- "test_size", QuickAdapterRegressorV3._TEST_SIZE
- ),
+ X_validation,
+ y_validation,
+ validation_weights,
+ validation_size,
)
model = fit_regressor(
train_weights=train_weights,
eval_set=eval_set,
eval_weights=eval_weights,
- model_training_parameters=model_training_parameters,
- init_model=self.get_init_model(dk.pair),
+ model_training_parameters=copy.deepcopy(model_training_parameters),
+ init_model=init_model,
model_path=dk.data_path,
)
+ if X_test is not None and not X_test.empty:
+ holdout_predictions = pd.DataFrame(
+ model.predict(X_test),
+ columns=y_test.columns,
+ index=y_test.index,
+ )
+ holdout_labels, _, _ = dk.label_pipeline.inverse_transform(y_test.copy())
+ holdout_predictions, _, _ = dk.label_pipeline.inverse_transform(
+ holdout_predictions
+ )
+ self._holdout_rmse[dk.pair] = float(
+ sklearn.metrics.root_mean_squared_error(
+ holdout_labels,
+ holdout_predictions,
+ sample_weight=test_weights,
+ )
+ )
+ else:
+ self._holdout_rmse[dk.pair] = np.inf
+ dk.data["extra_returns_per_train"]["holdout_rmse"] = self._holdout_rmse[dk.pair]
+ if validation_size != 0:
+ refit_model_training_parameters = get_refit_model_training_parameters(
+ self.regressor,
+ model,
+ model_training_parameters,
+ init_model,
+ )
+ data_dictionary["train_features"] = data_dictionary.pop("refit_features")
+ data_dictionary["train_labels"] = data_dictionary.pop("refit_labels")
+ data_dictionary["train_weights"] = data_dictionary.pop("refit_weights")
+ if (
+ not self.freqai_info.get("fit_live_predictions_candles", 0)
+ or not self.live
+ ):
+ dk.fit_labels()
+ (
+ data_dictionary["train_features"],
+ data_dictionary["train_labels"],
+ data_dictionary["train_weights"],
+ ) = self._fit_training_pipelines(
+ data_dictionary["train_features"],
+ data_dictionary["train_labels"],
+ data_dictionary["train_weights"],
+ dk,
+ dk.pair,
+ "refit",
+ )
+ logger.info(
+ f"[{dk.pair}] Refitting final model on "
+ f"{len(data_dictionary['train_features'])} causally available rows"
+ )
+ model = fit_regressor(
+ regressor=self.regressor,
+ X=data_dictionary["train_features"],
+ y=data_dictionary["train_labels"],
+ train_weights=data_dictionary["train_weights"],
+ eval_set=None,
+ eval_weights=None,
+ model_training_parameters=refit_model_training_parameters,
+ init_model=init_model,
+ model_path=dk.data_path,
+ )
time_spent = time.time() - start_time
self.dd.update_metric_tracker("fit_time", time_spent, dk.pair)
).get("label_natr_multiplier")
)
- hp_rmse = QuickAdapterRegressorV3.optuna_validate_value(
- self.get_optuna_value(pair, _OPTUNA_NAMESPACES.hp)
+ current_holdout_rmse = self._holdout_rmse.get(pair)
+ if not self.live:
+ holdout_series = dk.full_df.get("holdout_rmse")
+ history_dates = ensure_datetime_series(
+ self.dd.historic_predictions[pair]["date"]
+ )
+ full_dates = ensure_datetime_series(dk.full_df["date"])
+ current_dates = full_dates.loc[full_dates > history_dates.max()]
+ if holdout_series is None:
+ logger.warning(
+ f"[{pair}] 'holdout_rmse' column missing from full_df during "
+ f"replay; keeping the last in-memory value (may be stale)"
+ )
+ elif not current_dates.empty:
+ current_holdout_rmse = holdout_series.loc[current_dates.index[0]]
+ if pd.isna(current_holdout_rmse):
+ logger.warning(
+ f"[{pair}] replayed holdout_rmse is NaN at "
+ f"{current_dates.index[0]}; defaulting to inf"
+ )
+ holdout_rmse = QuickAdapterRegressorV3.optuna_validate_value(
+ current_holdout_rmse
)
- dk.data["extra_returns_per_train"]["hp_rmse"] = (
- hp_rmse if hp_rmse is not None else np.inf
+ dk.data["extra_returns_per_train"]["holdout_rmse"] = (
+ holdout_rmse if holdout_rmse is not None else np.inf
)
def _label_hpo_dataframe_as_of_prediction_time(
X: pd.DataFrame,
y: pd.DataFrame,
train_weights: NDArray[np.floating],
- X_test: pd.DataFrame,
- y_test: pd.DataFrame,
- test_weights: NDArray[np.floating],
- test_size: float,
+ X_validation: pd.DataFrame,
+ y_validation: pd.DataFrame,
+ validation_weights: NDArray[np.floating],
+ validation_size: float,
model_training_best_parameters: dict[str, Any],
model_training_parameters: dict[str, Any],
space_reduction: bool,
space_fraction: float,
model_path: Optional[Path] = None,
+ init_model: Any = None,
) -> float:
study_model_parameters = get_optuna_study_model_parameters(
trial,
model_training_parameters = {**model_training_parameters, **study_model_parameters}
eval_set, eval_weights = make_test_set_and_weights(
- X_test, y_test, test_weights, test_size
+ X_validation, y_validation, validation_weights, validation_size
)
model = fit_regressor(
eval_set=eval_set,
eval_weights=eval_weights,
model_training_parameters=model_training_parameters,
+ init_model=init_model,
model_path=model_path,
trial=trial,
)
- y_pred = model.predict(X_test)
+ y_pred = model.predict(X_validation)
return sklearn.metrics.root_mean_squared_error(
- y_test, y_pred, sample_weight=test_weights
+ y_validation, y_pred, sample_weight=validation_weights
)
return escaped
-def _format_collection(
- value: list | tuple | set,
- ctx: _FormatContext,
- depth: int,
- brackets: tuple[str, str],
- empty: str,
- trailing_comma: bool = False,
-) -> str:
- if not value:
- return empty
- obj_id = id(value)
- if obj_id in ctx.seen:
- return f"{brackets[0]}<circular>{brackets[1]}"
- if depth >= _MAX_DEPTH:
- return f"{brackets[0]}...{brackets[1]}"
- ctx.seen.add(obj_id)
- items_iter = sorted(value, key=str) if isinstance(value, set) else value
- items = [_format_value(v, ctx, depth + 1) for v in list(items_iter)[:_MAX_ITEMS]]
- if len(value) > _MAX_ITEMS:
- items.append(f"...+{len(value) - _MAX_ITEMS}")
- content = ", ".join(items)
- if trailing_comma and len(value) == 1 and len(items) == 1:
- content += ","
- ctx.seen.discard(obj_id)
- return f"{brackets[0]}{content}{brackets[1]}"
-
-
@_format_value.register(list)
def _(value: list, ctx: _FormatContext, depth: int) -> str:
return _format_collection(value, ctx, depth, ("[", "]"), "[]")
return f"array{value.shape}"
+def _format_collection(
+ value: list | tuple | set,
+ ctx: _FormatContext,
+ depth: int,
+ brackets: tuple[str, str],
+ empty: str,
+ trailing_comma: bool = False,
+) -> str:
+ if not value:
+ return empty
+ obj_id = id(value)
+ if obj_id in ctx.seen:
+ return f"{brackets[0]}<circular>{brackets[1]}"
+ if depth >= _MAX_DEPTH:
+ return f"{brackets[0]}...{brackets[1]}"
+ ctx.seen.add(obj_id)
+ items_iter = sorted(value, key=str) if isinstance(value, set) else value
+ items = [_format_value(v, ctx, depth + 1) for v in list(items_iter)[:_MAX_ITEMS]]
+ if len(value) > _MAX_ITEMS:
+ items.append(f"...+{len(value) - _MAX_ITEMS}")
+ content = ", ".join(items)
+ if trailing_comma and len(value) == 1 and len(items) == 1:
+ content += ","
+ ctx.seen.discard(obj_id)
+ return f"{brackets[0]}{content}{brackets[1]}"
+
+
def format_dict(
d: dict[str, Any],
style: Literal["dict", "params"] = "dict",
return dist_map[dist_name]
+def get_refit_model_training_parameters(
+ regressor: Regressor,
+ model: Any,
+ model_training_parameters: dict[str, Any],
+ init_model: Any = None,
+) -> dict[str, Any]:
+ """Return parameters that preserve the selected model capacity for refit."""
+ refit_parameters = copy.deepcopy(model_training_parameters)
+
+ if regressor == REGRESSORS[0]: # "xgboost"
+ fitted_iterations = int(model.get_booster().num_boosted_rounds())
+ initial_iterations = (
+ int(init_model.get_booster().num_boosted_rounds())
+ if init_model is not None
+ else 0
+ )
+ parameter_name = "n_estimators"
+ elif regressor == REGRESSORS[1]: # "lightgbm"
+ best_iteration = getattr(model, "best_iteration_", 0) or 0
+ fitted_iterations = int(
+ best_iteration if best_iteration > 0 else model.n_estimators_
+ )
+ initial_iterations = (
+ int(init_model.booster_.current_iteration())
+ if init_model is not None
+ else 0
+ )
+ parameter_name = "n_estimators"
+ elif regressor == REGRESSORS[2]: # "histgradientboostingregressor"
+ fitted_iterations = int(model.n_iter_)
+ initial_iterations = 0
+ parameter_name = "max_iter"
+ refit_parameters["early_stopping"] = False
+ elif regressor == REGRESSORS[3]: # "ngboost"
+ fitted_iterations = len(model.base_models)
+ initial_iterations = 0
+ parameter_name = "n_estimators"
+ elif regressor == REGRESSORS[4]: # "catboost"
+ fitted_iterations = int(model.tree_count_)
+ initial_iterations = 0
+ parameter_name = "iterations"
+ else:
+ raise ValueError(
+ f"Invalid regressor value {regressor!r}: "
+ f"supported values are {', '.join(REGRESSORS)}"
+ )
+
+ # best_iteration is combined-indexed under current xgboost/lightgbm, so
+ # fitted >= initial + 1 always holds; clamp defensively so a degenerate
+ # non-improving continual-learning refit degrades gracefully instead of
+ # raising and killing the whole training window.
+ refit_iterations = max(fitted_iterations - initial_iterations, 1)
+ refit_parameters[parameter_name] = refit_iterations
+ return refit_parameters
+
+
def fit_regressor(
regressor: Regressor,
X: pd.DataFrame,
model_training_parameters.setdefault("random_state", 1)
model_training_parameters.setdefault("loss", "squared_error")
- model_training_parameters.pop("early_stopping", None)
+ early_stopping = model_training_parameters.pop("early_stopping", True)
model_training_parameters.pop("n_jobs", None)
model_training_parameters.pop("l2_regularization_zero", None)
sample_weight_val = eval_weights[0]
model = HistGradientBoostingRegressor(
- early_stopping=True,
+ early_stopping=early_stopping,
scoring="neg_root_mean_squared_error",
**model_training_parameters,
)