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

    skf = StratifiedKFold(n_splits=args.cv, shuffle=True, random_state=0)
    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,
    }

    # Get numeric indices first
    numeric_indices = np.delete(np.arange(X.shape[1]), nominal_indices)

    # Numeric Preprocessing
    numeric_transformer = SimpleImputer(strategy="median")

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

    transformers = []
    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})"
    )

    # Store training indices (random state insures consistent across clfs)
    for train_index, test_index in skf.split(X, y):
        results_dict["test_indices"].append(test_index)

    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"]["train_times"] = []
        results_dict[f"{clf_name}_metadata"]["test_times"] = []
        for train_index, test_index in skf.split(X, y):
            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 = []
            oob_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)
            # oob_fold_probas.append(oob_probas_vs_sample_sizes)

        results_dict[clf_name] = fold_probas
        # results_dict[clf_name + '_oob'] = oob_fold_probas
        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}"
        )

    # 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)