def hyperparameters_validation(X, y, model, param_grid, scale=False, test_size=0.2, time_series=False, random_state=123, n_folds=5, regr=True):
    "\n    Select the best combination of values for the specified hyperparameters of the specified model on the given dataset.\n\n    In other words, perform the tuning of multiple hyperparameters.\n    The parameter `param_grid` is a dictionary that indicates which are the specified hyperparameters and what are the\n    associated values to test.\n\n    All the possible combinations of values are tested, in an exhaustive way (i.e. grid search).\n\n    This selection is made using the validation score (i.e. the best combination of hyperparameters values is the one with\n    the best validation score).\n    The validation score is computed by splitting the dataset into the training-test sets and then by applying the cross\n    validation on the training set.\n    Additionally, the training and test scores are also computed.\n\n    Parameters\n    ----------\n    X: np.array\n        Two-dimensional np.array, containing the explanatory features of the dataset.\n    y: np.array\n        Mono dimensional np.array, containing the response feature of the dataset.\n    model: sklearn.base.BaseEstimator\n        Model which has the specified hyperparameters.\n    param_grid: dict\n        Dictionary which has as keys the names of the specified hyperparameters and as values the associated list of\n        values to test.\n    scale: bool\n        Indicates whether to scale or not the features in `X`.\n        (The scaling is performed using the sklearn MinMaxScaler).\n    test_size: float\n        Decimal number between 0 and 1, which indicates the proportion of the test set.\n    time_series: bool\n        Indicates if the given dataset is a time series dataset (i.e. dataframe indexed by days).\n        (This affects the computing of the validation score).\n    random_state: int\n        Used in the training-test splitting of the dataset.\n    n_folds: int\n        Indicates how many folds are made in order to compute the k-fold cross validation.\n        (It's used only if `time_series` is False).\n    regr: bool\n        Indicates if it's either a regression or a classification problem.\n\n    Returns\n    ----------\n    params: list\n        List which enumerates all the possible combinations of hyperparameters values.\n        It's a list of dictionaries: each dictionary represents a specific combination of hyperparameters values. (It's a\n        dictionary which has as keys the hyperparameters names and as values the specific associated values of that combination).\n    train_val_scores: np.array\n        Two dimensional np.array, containing two columns: the first contains the training scores, the second the validation\n        scores.\n        It has as many rows as the number of possible combinations of the hyperparameters values.\n        (It has as many rows as the elements of `params`).\n    best_index: int\n        Index of `params` that indicates which is the best combination of hyperparameters values.\n    test_score: float\n        Test score associated with the best combination of hyperparameters values.\n\n    Notes\n    ----------\n    - If `regr` is True, the validation scores are errors (MSE, i.e. Mean Squared Errors): this means that the best\n      combination of hyperparameters values is the one associated with the minimum validation score.\n      Otherwise, the validation scores are accuracies: this means that the best combination of hyperparameters values is the\n      one associated with the maximum validation score.\n    - If `time_series` is False, the training-test splitting of the dataset is made randomly. In addition, the cross\n      validation strategy performed is the classic k-fold cross validation: the number of folds is specified by `n_folds`.\n      Otherwise, if `time_series` is True, the training-test sets are simply obtained by splitting the dataset into two\n      contiguous parts. In addition, the cross validation strategy performed is the sklearn TimeSeriesSplit.\n    "
    if regr:
        scoring = 'neg_mean_squared_error'
    else:
        scoring = 'accuracy'
    if (not time_series):
        (X_train_80, X_test, y_train_80, y_test) = train_test_split(X, y, test_size=test_size, random_state=random_state)
    else:
        train_len = int((X.shape[0] * (1 - test_size)))
        X_train_80 = X[:train_len]
        y_train_80 = y[:train_len]
        X_test = X[train_len:]
        y_test = y[train_len:]
    if scale:
        scaler = MinMaxScaler()
        scaler.fit(X_train_80)
        X_train_80 = scaler.transform(X_train_80)
        X_test = scaler.transform(X_test)
    if (not time_series):
        cv = n_folds
    else:
        cv = TimeSeriesSplit(n_splits=n_folds)
    grid_search = GridSearchCV(model, param_grid, scoring=scoring, cv=cv, return_train_score=True)
    grid_search.fit(X_train_80, y_train_80)
    params = grid_search.cv_results_['params']
    train_scores = grid_search.cv_results_['mean_train_score']
    val_scores = grid_search.cv_results_['mean_test_score']
    best_index = grid_search.best_index_
    best_model = grid_search.best_estimator_
    if regr:
        train_scores = (train_scores * (- 1))
        val_scores = (val_scores * (- 1))
    train_val_scores = np.concatenate((train_scores.reshape((- 1), 1), val_scores.reshape((- 1), 1)), axis=1)
    best_model.fit(X_train_80, y_train_80)
    test_score = 0
    if regr:
        test_score = mean_squared_error(y_true=y_test, y_pred=best_model.predict(X_test))
    else:
        test_score = accuracy_score(y_true=y_test, y_pred=best_model.predict(X_test))
    return (params, train_val_scores, best_index, test_score)
Select the best combination of values for the specified hyperparameters of the specified model on the given dataset.

In other words, perform the tuning of multiple hyperparameters.
The parameter `param_grid` is a dictionary that indicates which are the specified hyperparameters and what are the
associated values to test.

All the possible combinations of values are tested, in an exhaustive way (i.e. grid search).

This selection is made using the validation score (i.e. the best combination of hyperparameters values is the one with
the best validation score).
The validation score is computed by splitting the dataset into the training-test sets and then by applying the cross
validation on the training set.
Additionally, the training and test scores are also computed.

Parameters
----------
X: np.array
    Two-dimensional np.array, containing the explanatory features of the dataset.
y: np.array
    Mono dimensional np.array, containing the response feature of the dataset.
model: sklearn.base.BaseEstimator
    Model which has the specified hyperparameters.
param_grid: dict
    Dictionary which has as keys the names of the specified hyperparameters and as values the associated list of
    values to test.
scale: bool
    Indicates whether to scale or not the features in `X`.
    (The scaling is performed using the sklearn MinMaxScaler).
test_size: float
    Decimal number between 0 and 1, which indicates the proportion of the test set.
time_series: bool
    Indicates if the given dataset is a time series dataset (i.e. dataframe indexed by days).
    (This affects the computing of the validation score).
random_state: int
    Used in the training-test splitting of the dataset.
n_folds: int
    Indicates how many folds are made in order to compute the k-fold cross validation.
    (It's used only if `time_series` is False).
regr: bool
    Indicates if it's either a regression or a classification problem.

Returns
----------
params: list
    List which enumerates all the possible combinations of hyperparameters values.
    It's a list of dictionaries: each dictionary represents a specific combination of hyperparameters values. (It's a
    dictionary which has as keys the hyperparameters names and as values the specific associated values of that combination).
train_val_scores: np.array
    Two dimensional np.array, containing two columns: the first contains the training scores, the second the validation
    scores.
    It has as many rows as the number of possible combinations of the hyperparameters values.
    (It has as many rows as the elements of `params`).
best_index: int
    Index of `params` that indicates which is the best combination of hyperparameters values.
test_score: float
    Test score associated with the best combination of hyperparameters values.

Notes
----------
- If `regr` is True, the validation scores are errors (MSE, i.e. Mean Squared Errors): this means that the best
  combination of hyperparameters values is the one associated with the minimum validation score.
  Otherwise, the validation scores are accuracies: this means that the best combination of hyperparameters values is the
  one associated with the maximum validation score.
- If `time_series` is False, the training-test splitting of the dataset is made randomly. In addition, the cross
  validation strategy performed is the classic k-fold cross validation: the number of folds is specified by `n_folds`.
  Otherwise, if `time_series` is True, the training-test sets are simply obtained by splitting the dataset into two
  contiguous parts. In addition, the cross validation strategy performed is the sklearn TimeSeriesSplit.
def hyperparameters_validation(X, y, model, param_grid, scale=False, test_size=0.2, time_series=False, random_state=123, n_folds=5, regr=True):
    "\n    Select the best combination of values for the specified hyperparameters of the specified model on the given dataset.\n\n    In other words, perform the tuning of multiple hyperparameters.\n    The parameter `param_grid` is a dictionary that indicates which are the specified hyperparameters and what are the\n    associated values to test.\n\n    All the possible combinations of values are tested, in an exhaustive way (i.e. grid search).\n\n    This selection is made using the validation score (i.e. the best combination of hyperparameters values is the one with\n    the best validation score).\n    The validation score is computed by splitting the dataset into the training-test sets and then by applying the cross\n    validation on the training set.\n    Additionally, the training and test scores are also computed.\n\n    Parameters\n    ----------\n    X: np.array\n        Two-dimensional np.array, containing the explanatory features of the dataset.\n    y: np.array\n        Mono dimensional np.array, containing the response feature of the dataset.\n    model: sklearn.base.BaseEstimator\n        Model which has the specified hyperparameters.\n    param_grid: dict\n        Dictionary which has as keys the names of the specified hyperparameters and as values the associated list of\n        values to test.\n    scale: bool\n        Indicates whether to scale or not the features in `X`.\n        (The scaling is performed using the sklearn MinMaxScaler).\n    test_size: float\n        Decimal number between 0 and 1, which indicates the proportion of the test set.\n    time_series: bool\n        Indicates if the given dataset is a time series dataset (i.e. dataframe indexed by days).\n        (This affects the computing of the validation score).\n    random_state: int\n        Used in the training-test splitting of the dataset.\n    n_folds: int\n        Indicates how many folds are made in order to compute the k-fold cross validation.\n        (It's used only if `time_series` is False).\n    regr: bool\n        Indicates if it's either a regression or a classification problem.\n\n    Returns\n    ----------\n    params: list\n        List which enumerates all the possible combinations of hyperparameters values.\n        It's a list of dictionaries: each dictionary represents a specific combination of hyperparameters values. (It's a\n        dictionary which has as keys the hyperparameters names and as values the specific associated values of that combination).\n    train_val_scores: np.array\n        Two dimensional np.array, containing two columns: the first contains the training scores, the second the validation\n        scores.\n        It has as many rows as the number of possible combinations of the hyperparameters values.\n        (It has as many rows as the elements of `params`).\n    best_index: int\n        Index of `params` that indicates which is the best combination of hyperparameters values.\n    test_score: float\n        Test score associated with the best combination of hyperparameters values.\n\n    Notes\n    ----------\n    - If `regr` is True, the validation scores are errors (MSE, i.e. Mean Squared Errors): this means that the best\n      combination of hyperparameters values is the one associated with the minimum validation score.\n      Otherwise, the validation scores are accuracies: this means that the best combination of hyperparameters values is the\n      one associated with the maximum validation score.\n    - If `time_series` is False, the training-test splitting of the dataset is made randomly. In addition, the cross\n      validation strategy performed is the classic k-fold cross validation: the number of folds is specified by `n_folds`.\n      Otherwise, if `time_series` is True, the training-test sets are simply obtained by splitting the dataset into two\n      contiguous parts. In addition, the cross validation strategy performed is the sklearn TimeSeriesSplit.\n    "
    if regr:
        scoring = 'neg_mean_squared_error'
    else:
        scoring = 'accuracy'
    if (not time_series):
        (X_train_80, X_test, y_train_80, y_test) = train_test_split(X, y, test_size=test_size, random_state=random_state)
    else:
        train_len = int((X.shape[0] * (1 - test_size)))
        X_train_80 = X[:train_len]
        y_train_80 = y[:train_len]
        X_test = X[train_len:]
        y_test = y[train_len:]
    if scale:
        scaler = MinMaxScaler()
        scaler.fit(X_train_80)
        X_train_80 = scaler.transform(X_train_80)
        X_test = scaler.transform(X_test)
    if (not time_series):
        cv = n_folds
    else:
        cv = TimeSeriesSplit(n_splits=n_folds)
    grid_search = GridSearchCV(model, param_grid, scoring=scoring, cv=cv, return_train_score=True)
    grid_search.fit(X_train_80, y_train_80)
    params = grid_search.cv_results_['params']
    train_scores = grid_search.cv_results_['mean_train_score']
    val_scores = grid_search.cv_results_['mean_test_score']
    best_index = grid_search.best_index_
    best_model = grid_search.best_estimator_
    if regr:
        train_scores = (train_scores * (- 1))
        val_scores = (val_scores * (- 1))
    train_val_scores = np.concatenate((train_scores.reshape((- 1), 1), val_scores.reshape((- 1), 1)), axis=1)
    best_model.fit(X_train_80, y_train_80)
    test_score = 0
    if regr:
        test_score = mean_squared_error(y_true=y_test, y_pred=best_model.predict(X_test))
    else:
        test_score = accuracy_score(y_true=y_test, y_pred=best_model.predict(X_test))
    return (params, train_val_scores, best_index, test_score)