//  [python]
def train_test(X, y, task_name, task_id, nominal_indices, args, clfs, save_path, random_state):
    # Set up Cross validation

    n_samples, n_features = X.shape
    n_classes = len(np.unique(y))

    if args.vary_samples:
        sample_sizes = np.logspace(
            np.log10(n_classes * 2),
            np.log10(np.floor(len(y) * (args.cv - 1.1) / args.cv)),
            num=10,
            endpoint=True,
            dtype=int,
        )
    else:
        sample_sizes = [len(y)]

    # Check if existing experiments
    results_dict = {
        "task": task_name,
        "task_id": task_id,
        "n_samples": n_samples,
        "n_features": n_features,
        "n_classes": n_classes,
        "y": y,
        "test_indices": [],
        "n_estimators": args.n_estimators,
        "cv": args.cv,
        "nominal_features": len(nominal_indices),
        "sample_sizes": sample_sizes,
    }

    if len(nominal_indices) == n_features:
        print(f"Skipping task: {task_name}, {task_id} because all features are nominal")
        return

    # get the original numerical indices
    # numeric_indices = np.delete(np.arange(X.shape[1]), nominal_indices)

    # drop nominal indices
    X = np.delete(X, nominal_indices, axis=1)

    # Numeric Preprocessing
    numeric_transformer = Pipeline(
        steps=[
            ("imputer", SimpleImputer(strategy="median")),
            ("standardizer", StandardScaler()),
        ]
    )

    # Nominal preprocessing
    # nominal_transformer = Pipeline(
    #     steps=[
    #         ("imputer", SimpleImputer(strategy="most_frequent")),
    #         ("onehot", OneHotEncoder(handle_unknown="ignore", sparse=False)),
    #     ]
    # )

    transformers = []
    numeric_indices = np.arange(X.shape[1])
    # Get numeric indices first
    if len(numeric_indices) > 0:
        transformers += [("numeric", numeric_transformer, numeric_indices)]
    # if len(nominal_indices) > 0:
    #     transformers += [("nominal", nominal_transformer, nominal_indices)]
    preprocessor = ColumnTransformer(transformers=transformers)

    _, n_features_fitted = preprocessor.fit_transform(X, y).shape
    results_dict["n_features_fitted"] = n_features_fitted
    print(
        f"Features={n_features}, nominal={len(nominal_indices)} (After transforming={n_features_fitted})"
    )

    for clf_name, clf in clfs:
        pipeline = Pipeline(steps=[("Preprocessor", preprocessor), ("Estimator", clf)])

        fold_probas = []
        oob_fold_probas = []
        if not f"{clf_name}_metadata" in results_dict.keys():
            results_dict[f"{clf_name}_metadata"] = {}
        results_dict[f"{clf_name}_metadata"] = collections.defaultdict(list)
        # results_dict[f"{clf_name}_metadata"]["train_times"] = []
        # results_dict[f"{clf_name}_metadata"]["test_times"] = []

        # initialize the cross-validator
        skf = StratifiedKFold(n_splits=args.cv, shuffle=True, random_state=random_state)
        for train_index, test_index in skf.split(X, y):
            # Store training indices (random state insures consistent across clfs)
            results_dict[f"{clf_name}_metadata"]["test_indices"].append(test_index)

            X_train, X_test = X[train_index], X[test_index]
            y_train, y_test = y[train_index], y[test_index]

            if args.vary_samples:
                stratified_sort = stratify_samplesizes(y_train, sample_sizes)
                X_train = X_train[stratified_sort]
                y_train = y_train[stratified_sort]

            probas_vs_sample_sizes = []

            for n_samples in sample_sizes:
                start_time = time.time()
                # Fix too few samples for internal CV of these methods
                if (
                    clf_name in ["IRF", "SigRF"]
                    and np.min(np.unique(y_train[:n_samples], return_counts=True)[1]) < 5
                ):
                    print(
                        f"{clf_name} requires more samples of minimum class. Skipping n={n_samples}"
                    )
                    y_proba = np.repeat(
                        np.bincount(y_train[:n_samples]).reshape(1, -1) / len(y_train[:n_samples]),
                        X_test.shape[0],
                        axis=0,
                    )
                    # y_proba_oob = y_proba
                    train_time = time.time() - start_time
                else:
                    pipeline = pipeline.fit(X_train[:n_samples], y_train[:n_samples])
                    train_time = time.time() - start_time
                    y_proba = pipeline.predict_proba(X_test)
                    # y_proba_oob = predict_proba_oob(pipeline['Estimator'], pipeline['Preprocessor'].transform(X_train[:n_samples]))

                test_time = time.time() - (train_time + start_time)

                probas_vs_sample_sizes.append(y_proba)
                # oob_probas_vs_sample_sizes.append(y_proba_oob)
                results_dict[f"{clf_name}_metadata"]["train_times"].append(train_time)
                results_dict[f"{clf_name}_metadata"]["test_times"].append(test_time)

            fold_probas.append(probas_vs_sample_sizes)
            print(
                f"{clf_name} Time: train_time={train_time:.3f}, test_time={test_time:.3f}, Cohen Kappa={cohen_kappa_score(y_test, y_proba.argmax(1)):.3f}"
            )

        results_dict[clf_name] = fold_probas
    # If existing data, load and append to. Else save
    if os.path.isfile(save_path) and args.mode == "OVERWRITE":
        logging.info(f"OVERWRITING {task_name} ({task_id})")
        with open(save_path, "rb") as f:
            prior_results = pickle.load(f)

        # Check these keys have the same values
        verify_keys = [
            "task",
            "task_id",
            "n_samples",
            "n_features",
            "n_classes",
            "y",
            "test_indices",
            "n_estimators",
            "cv",
            "nominal_features",
            "n_features_fitted",
            "sample_sizes",
        ]
        for key in verify_keys:
            assert _check_nested_equality(
                prior_results[key], results_dict[key]
            ), f"OVERWRITE {key} does not match saved value"

        # Replace/add data
        replace_keys = [name for name, _ in clfs]
        replace_keys += [f"{name}_metadata" for name in replace_keys]
        for key in replace_keys:
            prior_results[key] = results_dict[key]

        results_dict = prior_results

    with open(save_path, "wb") as f:
        pickle.dump(results_dict, f)