"""This example is a loose HyperparameterHunter adaptation of the SKLearn example on the
"Effect of transforming the targets in regression models"
(https://scikit-learn.org/stable/auto_examples/compose/plot_transformed_target.html#real-world-data-set).
Specifically, we'll be looking at the section using the Boston Housing regression dataset, adapting
the target transformations therein to be used with HyperparameterHunter"""
##################################################
# Import Own Assets
##################################################
from hyperparameter_hunter import Environment, CVExperiment, FeatureEngineer
from hyperparameter_hunter import DummyOptPro, Categorical
from hyperparameter_hunter.utils.learning_utils import get_boston_data

##################################################
# Import Miscellaneous Assets
##################################################
import numpy as np

##################################################
# Import Learning Assets
##################################################
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import QuantileTransformer, StandardScaler


# noinspection PyUnusedLocal
def get_holdout_data(train, target_column):
    train_data, holdout_data = train_test_split(train, random_state=1)
    return train_data, holdout_data


##################################################
# Feature Engineering Steps
##################################################
def quantile_transform(train_targets, non_train_targets):
    transformer = QuantileTransformer(output_distribution="normal", n_quantiles=100)
    train_targets[train_targets.columns] = transformer.fit_transform(train_targets.values)
    non_train_targets[train_targets.columns] = transformer.transform(non_train_targets.values)
    return train_targets, non_train_targets, transformer


def log_transform(all_targets):
    all_targets = np.log1p(all_targets)
    return all_targets, np.expm1


def standard_scale(train_inputs, non_train_inputs):
    scaler = StandardScaler()
    train_inputs[train_inputs.columns] = scaler.fit_transform(train_inputs.values)
    non_train_inputs[train_inputs.columns] = scaler.transform(non_train_inputs.values)
    return train_inputs, non_train_inputs


def standard_scale_BAD(all_inputs):
    """If you wanted to standard-scale, by fitting on your entire dataset, rather than only on your
    train dataset (which is not recommended), this is how you could do it"""
    scaler = StandardScaler()
    all_inputs[all_inputs.columns] = scaler.fit_transform(all_inputs.values)
    return all_inputs


def square_sum_feature(all_inputs):
    all_inputs["square_sum"] = all_inputs.agg(
        lambda row: np.sqrt(np.sum([np.square(_) for _ in row])), axis="columns"
    )
    return all_inputs


##################################################
# Execute
##################################################
def execute():
    #################### Environment ####################
    env = Environment(
        train_dataset=get_boston_data(),
        results_path="HyperparameterHunterAssets",
        holdout_dataset=get_holdout_data,
        target_column="DIS",
        metrics=["r2_score", "median_absolute_error"],
        cv_type="KFold",
        cv_params=dict(n_splits=10, random_state=1),
    )

    #################### CVExperiment ####################
    exp_0 = CVExperiment(
        model_initializer=Ridge,
        model_init_params=dict(),
        feature_engineer=FeatureEngineer([quantile_transform]),
    )

    #################### Optimization ####################
    # `opt_0` recognizes `exp_0`'s `feature_engineer` and its results as valid learning material
    # This is because `opt_0` marks the engineer step functions omitted by `exp_0` as `optional=True`
    opt_0 = DummyOptPro(iterations=10)
    opt_0.forge_experiment(
        model_initializer=Ridge,
        model_init_params=dict(),
        feature_engineer=FeatureEngineer(
            [
                Categorical([quantile_transform, log_transform], optional=True),
                Categorical([standard_scale, standard_scale_BAD], optional=True),
                Categorical([square_sum_feature], optional=True),
            ]
        ),
    )
    opt_0.go()


if __name__ == "__main__":
    execute()
